@seatlayer/js 0.36.3 → 0.38.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -8,6 +8,14 @@ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
8
  var __typeError = (msg) => {
9
9
  throw TypeError(msg);
10
10
  };
11
+ var __esm = (fn, res, err) => function __init() {
12
+ if (err) throw err[0];
13
+ try {
14
+ return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
15
+ } catch (e) {
16
+ throw err = [e], e;
17
+ }
18
+ };
11
19
  var __export = (target, all) => {
12
20
  for (var name in all)
13
21
  __defProp(target, name, { get: all[name], enumerable: true });
@@ -35,6 +43,354 @@ var __privateAdd = (obj, member, value) => member.has(obj) ? __typeError("Cannot
35
43
  var __privateSet = (obj, member, value, setter) => (__accessCheck(obj, member, "write to private field"), setter ? setter.call(obj, value) : member.set(obj, value), value);
36
44
  var __privateMethod = (obj, member, method) => (__accessCheck(obj, member, "access private method"), method);
37
45
 
46
+ // src/hostedCheckout.ts
47
+ var hostedCheckout_exports = {};
48
+ __export(hostedCheckout_exports, {
49
+ errorCopy: () => errorCopy,
50
+ formatMoney: () => formatMoney,
51
+ mountCheckout: () => mountCheckout,
52
+ unavailableCopy: () => unavailableCopy
53
+ });
54
+ function ensureStyle() {
55
+ if (typeof document === "undefined" || document.getElementById(STYLE_ID)) return;
56
+ const el2 = document.createElement("style");
57
+ el2.id = STYLE_ID;
58
+ el2.textContent = CSS;
59
+ document.head.appendChild(el2);
60
+ }
61
+ function formatMoney(amount, currency) {
62
+ try {
63
+ return new Intl.NumberFormat(void 0, { style: "currency", currency }).format(amount);
64
+ } catch {
65
+ return `${amount.toFixed(2)} ${currency}`;
66
+ }
67
+ }
68
+ function unavailableCopy(reason, seatCount) {
69
+ const held = `${seatCount} ${seatCount === 1 ? "seat is" : "seats are"} held for a limited time.`;
70
+ if (reason === "payments_off_for_event") {
71
+ return {
72
+ title: "This event isn\u2019t sold online",
73
+ body: `${held} The organiser isn\u2019t taking payment for this event here.`,
74
+ detail: "Nothing has been charged. Check where you found this event for how to get tickets."
75
+ };
76
+ }
77
+ if (reason === "unavailable_for_event") {
78
+ return {
79
+ title: "This event isn\u2019t taking payment yet",
80
+ body: `${held} Online payment is not switched on for this event.`,
81
+ detail: "Nothing has been charged. If you were sent here to pay, let the organiser know \u2014 only they can turn payment on for this event."
82
+ };
83
+ }
84
+ return {
85
+ title: "Finish in the ticketing checkout",
86
+ body: `${held} Payment for this event is taken elsewhere.`,
87
+ detail: "Nothing has been charged. Continue in the checkout on this page to pay for your seats."
88
+ };
89
+ }
90
+ function errorCopy(code) {
91
+ switch (code) {
92
+ case "gateway_not_connected":
93
+ case "payments_not_enabled_for_event":
94
+ return "This event is not taking online payments. Contact the organiser to buy these seats.";
95
+ case "provider_mismatch":
96
+ return "This event\u2019s payment setup changed while you were choosing. Nothing was charged \u2014 please try again.";
97
+ case "hold_not_active":
98
+ case "hold_not_found":
99
+ return "Your seats were released before checkout started. Nothing was charged \u2014 please pick again.";
100
+ case "checkout_already_started":
101
+ return "A payment for these seats is already in progress. Finish that one, or wait for it to time out before starting again.";
102
+ case "event_closed":
103
+ return "Sales for this event have closed.";
104
+ case "gateway_currency_mismatch":
105
+ case "unsupported_currency":
106
+ case "mixed_currency_hold":
107
+ case "price_unusable":
108
+ return "These seats cannot be checked out right now because of a pricing configuration problem. Nothing was charged \u2014 please contact the organiser.";
109
+ case "rate_limited":
110
+ return "Too many attempts. Wait a moment and try again.";
111
+ default:
112
+ return "We could not start the payment. Nothing was charged \u2014 please try again.";
113
+ }
114
+ }
115
+ function loadRazorpay() {
116
+ const existing = window.Razorpay;
117
+ if (existing) return Promise.resolve(existing);
118
+ return new Promise((resolve, reject) => {
119
+ const previous = document.querySelector(`script[src="${RAZORPAY_SCRIPT}"]`);
120
+ const tag = previous ?? document.createElement("script");
121
+ const done = () => {
122
+ const ctor = window.Razorpay;
123
+ if (ctor) resolve(ctor);
124
+ else reject(new Error("razorpay_unavailable"));
125
+ };
126
+ tag.addEventListener("load", done, { once: true });
127
+ tag.addEventListener("error", () => reject(new Error("razorpay_script_failed")), { once: true });
128
+ if (previous) return;
129
+ tag.src = RAZORPAY_SCRIPT;
130
+ tag.async = true;
131
+ document.head.appendChild(tag);
132
+ });
133
+ }
134
+ function el(tag, className, text) {
135
+ const node = document.createElement(tag);
136
+ if (className) node.className = className;
137
+ if (text !== void 0) node.textContent = text;
138
+ return node;
139
+ }
140
+ function mountCheckout(mount) {
141
+ ensureStyle();
142
+ let live = true;
143
+ let confirmed = false;
144
+ const scrim = el("div", "sl-hco");
145
+ scrim.setAttribute("role", "dialog");
146
+ scrim.setAttribute("aria-modal", "true");
147
+ const card = el("div", "sl-hco-card");
148
+ scrim.appendChild(card);
149
+ const destroy = () => {
150
+ live = false;
151
+ scrim.remove();
152
+ document.removeEventListener("keydown", onKey, true);
153
+ };
154
+ const cancel = () => {
155
+ destroy();
156
+ mount.onCancel();
157
+ };
158
+ function onKey(event) {
159
+ if (event.key !== "Escape" || !scrim.isConnected) return;
160
+ event.stopPropagation();
161
+ event.preventDefault();
162
+ cancel();
163
+ }
164
+ document.addEventListener("keydown", onKey, true);
165
+ const title = el("h2", "sl-hco-title", "Checkout");
166
+ const titleId = `sl-hco-t-${Math.random().toString(36).slice(2, 8)}`;
167
+ title.id = titleId;
168
+ scrim.setAttribute("aria-labelledby", titleId);
169
+ const show = (...nodes) => {
170
+ card.replaceChildren(title, ...nodes);
171
+ };
172
+ const fail = (message) => {
173
+ if (!live) return;
174
+ const status = el("p", "sl-hco-status sl-hco-error", message);
175
+ status.setAttribute("role", "alert");
176
+ const back2 = el("button", "sl-hco-back", "Back to seats");
177
+ back2.type = "button";
178
+ back2.addEventListener("click", cancel);
179
+ show(status, back2);
180
+ };
181
+ const waiting = (message) => {
182
+ if (!live) return;
183
+ const status = el("p", "sl-hco-status", message);
184
+ status.setAttribute("role", "status");
185
+ show(status);
186
+ };
187
+ const awaitConfirmation = async (orderId) => {
188
+ waiting("Payment received \u2014 confirming your seats\u2026");
189
+ const deadline = Date.now() + CONFIRM_TIMEOUT_MS;
190
+ while (live && Date.now() < deadline) {
191
+ try {
192
+ const body = await mount.orderStatus(orderId);
193
+ if (!live) return;
194
+ if (body.status === "confirmed") {
195
+ confirmed = true;
196
+ title.textContent = "Your tickets are confirmed";
197
+ const status = el(
198
+ "p",
199
+ "sl-hco-status",
200
+ `${body.seatCount} ${body.seatCount === 1 ? "seat" : "seats"} confirmed. We have emailed your tickets.`
201
+ );
202
+ const receipt = el("dl", "sl-hco-receipt");
203
+ receipt.append(
204
+ el("dt", void 0, "Paid"),
205
+ el("dd", void 0, `${body.amountFormatted} ${body.currency}`),
206
+ el("dt", void 0, "Order"),
207
+ el("dd", "sl-hco-ref", body.orderId)
208
+ );
209
+ const close = el("button", "sl-hco-back", "Close");
210
+ close.type = "button";
211
+ close.addEventListener("click", cancel);
212
+ show(status, receipt, close);
213
+ mount.onConfirmed(body);
214
+ return;
215
+ }
216
+ if (body.status === "failed" || body.status === "expired") {
217
+ fail(body.status === "expired" ? "Your seats were released before payment completed. Nothing was charged \u2014 please pick again." : "The payment did not complete. If you were charged, it has been refunded automatically.");
218
+ return;
219
+ }
220
+ } catch {
221
+ }
222
+ await new Promise((resolve) => setTimeout(resolve, CONFIRM_POLL_MS));
223
+ }
224
+ if (!live || confirmed) return;
225
+ fail("Still confirming with the payment provider. If your payment went through, your tickets will arrive by email shortly \u2014 you do not need to pay again.");
226
+ };
227
+ if (mount.state.kind === "unavailable") {
228
+ const copy = unavailableCopy(mount.state.reason, mount.state.seatCount);
229
+ title.textContent = copy.title;
230
+ const kicker = el("p", "sl-hco-label", "Seats held");
231
+ const back2 = el("button", "sl-hco-back", "Back to seat map");
232
+ back2.type = "button";
233
+ back2.addEventListener("click", cancel);
234
+ card.replaceChildren(
235
+ kicker,
236
+ title,
237
+ el("p", "sl-hco-status", copy.body),
238
+ el("p", "sl-hco-note", copy.detail),
239
+ back2
240
+ );
241
+ mount.root.appendChild(scrim);
242
+ back2.focus();
243
+ return { destroy };
244
+ }
245
+ if (mount.state.kind === "resume") {
246
+ mount.root.appendChild(scrim);
247
+ void awaitConfirmation(mount.state.orderId);
248
+ return { destroy };
249
+ }
250
+ const { order, provider } = mount.state;
251
+ const summary = el("div", "sl-hco-summary");
252
+ const seats = el("div", "sl-hco-seats");
253
+ for (const label of order.labels) seats.appendChild(el("span", "sl-hco-seat", label));
254
+ const totalText = formatMoney(order.total, order.currency);
255
+ const totalRow = el("div", "sl-hco-total");
256
+ totalRow.append(el("span", void 0, "Total"), el("strong", void 0, totalText));
257
+ summary.append(seats, totalRow);
258
+ const form = el("form", "sl-hco-form");
259
+ const emailLabel = el("label", "sl-hco-label", "Email \u2014 your tickets go here");
260
+ const email = el("input", "sl-hco-input");
261
+ email.type = "email";
262
+ email.required = true;
263
+ email.autocomplete = "email";
264
+ email.placeholder = "you@example.com";
265
+ email.id = `${titleId}-email`;
266
+ emailLabel.htmlFor = email.id;
267
+ const nameLabel = el("label", "sl-hco-label", "Name (optional)");
268
+ const name = el("input", "sl-hco-input");
269
+ name.type = "text";
270
+ name.autocomplete = "name";
271
+ name.placeholder = "Your name";
272
+ name.id = `${titleId}-name`;
273
+ nameLabel.htmlFor = name.id;
274
+ const pay = el("button", "sl-hco-pay", `Pay ${totalText}`);
275
+ pay.type = "submit";
276
+ pay.disabled = true;
277
+ const back = el("button", "sl-hco-back", "Back to seats");
278
+ back.type = "button";
279
+ back.addEventListener("click", cancel);
280
+ const until = new Date(order.expiresAt).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" });
281
+ const note = el(
282
+ "p",
283
+ "sl-hco-note",
284
+ `Your seats are held until ${until}. Payment is handled by ${provider === "razorpay" ? "Razorpay" : "Stripe"} \u2014 we never see your card details.`
285
+ );
286
+ const emailValid = () => /.+@.+\..+/.test(email.value.trim());
287
+ email.addEventListener("input", () => {
288
+ pay.disabled = !emailValid();
289
+ });
290
+ const start = async () => {
291
+ waiting("Opening secure payment\u2026");
292
+ try {
293
+ const body = await mount.startSession({
294
+ holdId: order.holdId,
295
+ buyerEmail: email.value.trim(),
296
+ // Deliberately no `provider`: since W2 the EVENT row decides which
297
+ // gateway charges, and naming one here can only ever 409 on a mismatch
298
+ // the buyer cannot do anything about.
299
+ ...name.value.trim() ? { buyerName: name.value.trim() } : {}
300
+ });
301
+ if (!live) return;
302
+ if (body.redirectUrl) {
303
+ waiting("Taking you to secure payment\u2026");
304
+ window.location.assign(body.redirectUrl);
305
+ return;
306
+ }
307
+ if (body.clientPayload) {
308
+ const payload = body.clientPayload;
309
+ const Razorpay = await loadRazorpay();
310
+ if (!live) return;
311
+ waiting("Waiting for payment\u2026");
312
+ new Razorpay({
313
+ key: payload.key,
314
+ order_id: payload.orderId,
315
+ amount: payload.amount,
316
+ currency: payload.currency,
317
+ name: payload.name,
318
+ prefill: payload.prefill,
319
+ // The handler fires on the browser's word alone, so it only starts the
320
+ // wait — the webhook is what actually confirms the order.
321
+ handler: () => {
322
+ void awaitConfirmation(body.orderId);
323
+ },
324
+ modal: { ondismiss: () => {
325
+ if (live) details();
326
+ } }
327
+ }).open();
328
+ return;
329
+ }
330
+ fail(errorCopy("gateway_unavailable"));
331
+ } catch (err) {
332
+ mount.onError?.(err);
333
+ fail(errorCopy(err?.code));
334
+ }
335
+ };
336
+ form.addEventListener("submit", (event) => {
337
+ event.preventDefault();
338
+ if (emailValid()) void start();
339
+ });
340
+ form.append(emailLabel, email, nameLabel, name, pay, back, note);
341
+ const details = () => {
342
+ title.textContent = "Checkout";
343
+ show(summary, form);
344
+ pay.disabled = !emailValid();
345
+ };
346
+ details();
347
+ mount.root.appendChild(scrim);
348
+ email.focus();
349
+ return { destroy };
350
+ }
351
+ var CONFIRM_TIMEOUT_MS, CONFIRM_POLL_MS, RAZORPAY_SCRIPT, STYLE_ID, CSS;
352
+ var init_hostedCheckout = __esm({
353
+ "src/hostedCheckout.ts"() {
354
+ "use strict";
355
+ CONFIRM_TIMEOUT_MS = 9e4;
356
+ CONFIRM_POLL_MS = 2e3;
357
+ RAZORPAY_SCRIPT = "https://checkout.razorpay.com/v1/checkout.js";
358
+ STYLE_ID = "seatlayer-checkout-style";
359
+ CSS = /* @sl-css */
360
+ `
361
+ .sl-hco{position:absolute;inset:0;z-index:60;display:flex;align-items:center;justify-content:center;
362
+ padding:16px;background:color-mix(in srgb, var(--sl-bg) 82%, transparent);backdrop-filter:blur(3px)}
363
+ .sl-hco-card{width:100%;max-width:380px;max-height:100%;overflow:auto;padding:22px;
364
+ background:var(--sl-surface);color:var(--sl-text);border:1px solid var(--sl-line);
365
+ border-radius:var(--sl-radius);box-shadow:0 18px 48px rgba(0,0,0,.28)}
366
+ .sl-hco-title{margin:0 0 14px;font-size:19px;font-weight:650;letter-spacing:-.01em}
367
+ .sl-hco-summary{margin-bottom:16px;padding-bottom:14px;border-bottom:1px solid var(--sl-line)}
368
+ .sl-hco-seats{display:flex;flex-wrap:wrap;gap:6px;margin-bottom:10px}
369
+ .sl-hco-seat{padding:3px 8px;font-size:12px;font-weight:600;border-radius:calc(var(--sl-radius) * .5);
370
+ background:var(--sl-bg);border:1px solid var(--sl-line)}
371
+ .sl-hco-total{display:flex;justify-content:space-between;align-items:baseline;font-size:14px}
372
+ .sl-hco-total strong{font-size:17px;font-weight:700}
373
+ .sl-hco-label{display:block;margin:12px 0 5px;font-size:12px;font-weight:600;color:var(--sl-muted)}
374
+ .sl-hco-input{width:100%;padding:10px 11px;font:inherit;font-size:15px;color:var(--sl-text);
375
+ background:var(--sl-bg);border:1px solid var(--sl-line);border-radius:calc(var(--sl-radius) * .55)}
376
+ .sl-hco-input:focus-visible{outline:2px solid var(--sl-accent);outline-offset:1px}
377
+ .sl-hco-pay{width:100%;margin-top:16px;padding:12px;font:inherit;font-size:15px;font-weight:650;
378
+ color:var(--sl-accent-ink);background:var(--sl-accent);border:0;
379
+ border-radius:calc(var(--sl-radius) * .55);cursor:pointer}
380
+ .sl-hco-pay[disabled]{opacity:.5;cursor:default}
381
+ .sl-hco-back{width:100%;margin-top:8px;padding:10px;font:inherit;font-size:14px;color:var(--sl-muted);
382
+ background:none;border:0;cursor:pointer;text-decoration:underline}
383
+ .sl-hco-note{margin:12px 0 0;font-size:12px;line-height:1.5;color:var(--sl-muted)}
384
+ .sl-hco-status{margin:8px 0 0;font-size:14px;line-height:1.55}
385
+ .sl-hco-error{color:var(--sl-danger, #c0392b)}
386
+ .sl-hco-receipt{display:grid;grid-template-columns:auto 1fr;gap:4px 14px;margin:14px 0 0;font-size:13px}
387
+ .sl-hco-receipt dt{color:var(--sl-muted)}
388
+ .sl-hco-receipt dd{margin:0;text-align:right}
389
+ .sl-hco-ref{font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:12px;word-break:break-all}
390
+ `;
391
+ }
392
+ });
393
+
38
394
  // src/index.ts
39
395
  var index_exports = {};
40
396
  __export(index_exports, {
@@ -185,6 +541,41 @@ var PubApi = class {
185
541
  body: { holdId, ...ttlMs ? { ttlMs } : {} }
186
542
  });
187
543
  }
544
+ /**
545
+ * Which gateways this event can actually take money through — the question
546
+ * `checkout: 'hosted'` has to answer BEFORE it shows a buyer a Pay button, so
547
+ * the answer is never discovered by failing a payment.
548
+ *
549
+ * Anonymous, and it discloses no account, key, mode or currency for a gateway
550
+ * that did not match.
551
+ */
552
+ paymentOptions(key) {
553
+ return this.request(`/pub/events/${encodeURIComponent(key)}/payment-options`);
554
+ }
555
+ /**
556
+ * Turn a live hold into an order and start a payment.
557
+ *
558
+ * The amount is NOT sent: the server recomputes it from the hold's own items,
559
+ * which is the only reason a browser cannot alter what it pays. Nor is the
560
+ * PROVIDER — the event row decides which gateway charges, and a `provider` in
561
+ * the body is checked rather than obeyed (409 `provider_mismatch`). Omitting
562
+ * it is the shape that cannot disagree.
563
+ */
564
+ startCheckout(key, input) {
565
+ return this.request(`/pub/events/${encodeURIComponent(key)}/checkout`, {
566
+ method: "POST",
567
+ body: input
568
+ });
569
+ }
570
+ /**
571
+ * Poll an order while its gateway webhook lands. The order id is an
572
+ * unguessable token the buyer already holds, so it acts as the capability —
573
+ * which is also why a buyer returning from a gateway page can be told what
574
+ * happened with nothing but the id in the return URL.
575
+ */
576
+ orderStatus(orderId) {
577
+ return this.request(`/pub/orders/${encodeURIComponent(orderId)}/status`);
578
+ }
188
579
  /**
189
580
  * Mint a one-use subscribe ticket for the next socket attempt (protocol doc
190
581
  * §3). The bearer travels here, over ordinary HTTPS where CORS and Origin
@@ -845,9 +1236,9 @@ var DEFAULT_API_BASE = "https://api.seatlayer.io";
845
1236
  var DEFAULT_MAX_SELECTION = 10;
846
1237
  function resolveContainer(container) {
847
1238
  if (typeof container === "string") {
848
- const el = document.querySelector(container);
849
- if (!el) throw new Error(`seatmap: container "${container}" not found`);
850
- return el;
1239
+ const el2 = document.querySelector(container);
1240
+ if (!el2) throw new Error(`seatmap: container "${container}" not found`);
1241
+ return el2;
851
1242
  }
852
1243
  if (!(container instanceof HTMLElement)) {
853
1244
  throw new Error("seatmap: container must be a CSS selector or an HTMLElement");
@@ -1738,7 +2129,8 @@ var EmbeddedDesigner = class {
1738
2129
  }
1739
2130
  buildSkeleton(overlay) {
1740
2131
  const style = document.createElement("style");
1741
- style.textContent = `
2132
+ style.textContent = /* @sl-css */
2133
+ `
1742
2134
  @media (prefers-reduced-motion: no-preference) {
1743
2135
  @keyframes seatlayer-designer-shimmer {
1744
2136
  0% { background-position: -320px 0; }
@@ -1877,9 +2269,9 @@ function pointInPolygon(x, y, poly) {
1877
2269
  }
1878
2270
  function resolveContainer3(container) {
1879
2271
  if (typeof container === "string") {
1880
- const el = document.querySelector(container);
1881
- if (!el) throw new Error(`seatmap: container "${container}" not found`);
1882
- return el;
2272
+ const el2 = document.querySelector(container);
2273
+ if (!el2) throw new Error(`seatmap: container "${container}" not found`);
2274
+ return el2;
1883
2275
  }
1884
2276
  if (!(container instanceof HTMLElement)) {
1885
2277
  throw new Error("seatmap: container must be a CSS selector or an HTMLElement");
@@ -1918,20 +2310,45 @@ function hasWebGL2() {
1918
2310
  }
1919
2311
  return _webgl2Cache;
1920
2312
  }
2313
+ function cdnChunkUrl(fileName) {
2314
+ const base = SEATLAYER_MODULE_URL ?? (typeof location !== "undefined" ? location.href : void 0);
2315
+ if (!base) throw new Error(`seatlayer: cannot resolve the ${fileName} chunk URL`);
2316
+ return new URL(`./${fileName}`, base).href;
2317
+ }
1921
2318
  async function loadVenue3d() {
1922
2319
  if (typeof __SEATLAYER_CDN__ !== "undefined" && __SEATLAYER_CDN__) {
1923
- const base = SEATLAYER_MODULE_URL ?? (typeof location !== "undefined" ? location.href : void 0);
1924
- if (!base) throw new Error("seatlayer: cannot resolve the 3D view chunk URL");
1925
- const url = new URL("./seatlayer-view3d.mjs", base).href;
1926
2320
  return import(
1927
2321
  /* @vite-ignore */
1928
- url
2322
+ cdnChunkUrl("seatlayer-view3d.mjs")
1929
2323
  );
1930
2324
  }
1931
2325
  return import("@seatlayer/core/view3d");
1932
2326
  }
1933
- var STYLE_ID = "seatlayer-picker-style";
1934
- var CSS = `
2327
+ async function loadPanorama() {
2328
+ if (typeof __SEATLAYER_CDN__ !== "undefined" && __SEATLAYER_CDN__) {
2329
+ return import(
2330
+ /* @vite-ignore */
2331
+ cdnChunkUrl("seatlayer-panorama.mjs")
2332
+ );
2333
+ }
2334
+ return import("@seatlayer/core");
2335
+ }
2336
+ async function loadHostedCheckout() {
2337
+ if (typeof __SEATLAYER_CDN__ !== "undefined" && __SEATLAYER_CDN__) {
2338
+ return import(
2339
+ /* @vite-ignore */
2340
+ cdnChunkUrl("seatlayer-checkout.mjs")
2341
+ );
2342
+ }
2343
+ return Promise.resolve().then(() => (init_hostedCheckout(), hostedCheckout_exports));
2344
+ }
2345
+ function paymentsOffReason(reason) {
2346
+ return reason === "unavailable_for_event" || reason === "payments_off_for_event" ? reason : "not_configured";
2347
+ }
2348
+ var STYLE_ID2 = "seatlayer-picker-style";
2349
+ var CSS2 = (
2350
+ /* @sl-css */
2351
+ `
1935
2352
  .sl-picker{position:relative;display:flex;flex-direction:column;width:100%;height:100%;min-height:420px;overflow:hidden;
1936
2353
  background:var(--sl-bg);color:var(--sl-text);font-family:var(--sl-font);border-radius:var(--sl-radius);
1937
2354
  --sl-r-sm:calc(var(--sl-radius) * .55);
@@ -2668,13 +3085,14 @@ var CSS = `
2668
3085
  .sl-modal-scrim{position:fixed;inset:0;z-index:2147483000;background:rgba(5,7,12,.66);display:flex;align-items:center;justify-content:center;padding:18px}
2669
3086
  .sl-modal-frame{width:min(1200px,100%);height:min(820px,100%);border-radius:16px;overflow:hidden;box-shadow:0 40px 120px -30px rgba(0,0,0,.8)}
2670
3087
  @media(max-width:640px){.sl-modal-scrim{padding:0}.sl-modal-frame{width:100%;height:100%;border-radius:0}}
2671
- `;
2672
- function ensureStyle() {
2673
- if (document.getElementById(STYLE_ID)) return;
2674
- const el = document.createElement("style");
2675
- el.id = STYLE_ID;
2676
- el.textContent = CSS;
2677
- document.head.appendChild(el);
3088
+ `
3089
+ );
3090
+ function ensureStyle2() {
3091
+ if (document.getElementById(STYLE_ID2)) return;
3092
+ const el2 = document.createElement("style");
3093
+ el2.id = STYLE_ID2;
3094
+ el2.textContent = CSS2;
3095
+ document.head.appendChild(el2);
2678
3096
  }
2679
3097
  function resolveTokens(chart, host) {
2680
3098
  const accent = host?.accent ?? chart?.accent ?? "#f4b740";
@@ -2733,6 +3151,14 @@ var SeatPicker = class _SeatPicker {
2733
3151
  this.handedOff = false;
2734
3152
  /** Guards single onBooked + single success overlay per hold. */
2735
3153
  this.bookedShown = false;
3154
+ /**
3155
+ * In-flight or settled `payment-options` for this event, started at render in
3156
+ * hosted mode. One request, kicked off while the buyer is still choosing, so
3157
+ * pressing Pay does not wait on a lookup whose answer never changes mid-session.
3158
+ */
3159
+ this.paymentOptions = null;
3160
+ /** The mounted payment card, while one is up. */
3161
+ this.checkoutPanel = null;
2736
3162
  this.extendEl = null;
2737
3163
  this.bookedEl = null;
2738
3164
  this.gaQty = /* @__PURE__ */ new Map();
@@ -2847,6 +3273,12 @@ var SeatPicker = class _SeatPicker {
2847
3273
  onObjectUnavailable: (event) => this.opts.onSelectedObjectUnavailable?.(event)
2848
3274
  });
2849
3275
  this.api = options.transport ?? this.pubApi;
3276
+ if (options.checkout === "hosted" && !this.pubApi) {
3277
+ console.warn(
3278
+ 'seatlayer: checkout: "hosted" needs the widget\'s own transport \u2014 a custom `transport` owns its backend, so the picker is staying on onCheckout for this mount.'
3279
+ );
3280
+ }
3281
+ this.checkoutMode = options.checkout === "hosted" && this.pubApi ? "hosted" : "handoff";
2850
3282
  this.maxTickets = Math.max(1, Math.floor(options.maxSelection ?? DEFAULT_MAX_SELECTION2));
2851
3283
  this.cbSafe = readStoredColorblind() ?? !!options.colorblindSafe;
2852
3284
  this.controller = new import_core2.PickerController({
@@ -3152,7 +3584,7 @@ var SeatPicker = class _SeatPicker {
3152
3584
  }
3153
3585
  /** Mount the full picker as a document-level modal. Resolves after render. */
3154
3586
  static async open(options) {
3155
- ensureStyle();
3587
+ ensureStyle2();
3156
3588
  const scrim = document.createElement("div");
3157
3589
  scrim.className = "sl-modal-scrim";
3158
3590
  const frame = document.createElement("div");
@@ -3207,9 +3639,10 @@ var SeatPicker = class _SeatPicker {
3207
3639
  async render() {
3208
3640
  if (this.rendered) return this;
3209
3641
  this.rendered = true;
3210
- ensureStyle();
3642
+ ensureStyle2();
3211
3643
  await (0, import_core2.loadLocale)(this.opts.locale);
3212
3644
  if (this.opts.messages) (0, import_core2.setStringOverrides)(this.opts.messages);
3645
+ if (this.checkoutMode === "hosted") this.paymentOptions = this.pubApi.paymentOptions(this.opts.event);
3213
3646
  const mount = resolveContainer3(this.opts.container);
3214
3647
  const root = document.createElement("div");
3215
3648
  root.className = "sl-picker";
@@ -3294,8 +3727,8 @@ var SeatPicker = class _SeatPicker {
3294
3727
  </div>
3295
3728
  </div>
3296
3729
  </div>`;
3297
- root.querySelectorAll("[data-ref]").forEach((el) => {
3298
- this.els[el.dataset.ref] = el;
3730
+ root.querySelectorAll("[data-ref]").forEach((el2) => {
3731
+ this.els[el2.dataset.ref] = el2;
3299
3732
  });
3300
3733
  this.mapHost = this.els.map;
3301
3734
  const applyLayout = () => {
@@ -3507,8 +3940,40 @@ var SeatPicker = class _SeatPicker {
3507
3940
  if (this.salesClosed) this.applySalesClosed();
3508
3941
  this.syncPrices();
3509
3942
  this.syncTray();
3943
+ this.resumeHostedOrder();
3510
3944
  return this;
3511
3945
  }
3946
+ /**
3947
+ * A hosted gateway returned this buyer to a page that runs the widget, with
3948
+ * `?order=…&status=…` in the URL. Pick the order up and finish the story.
3949
+ *
3950
+ * Only `success` resumes. `cancelled` means the buyer backed out at the
3951
+ * gateway and their seats are still held — the map they are looking at IS the
3952
+ * right screen, and opening a card to say "you cancelled" would be noise.
3953
+ *
3954
+ * The two parameters are then stripped with `replaceState`, because they are a
3955
+ * one-shot instruction: leaving them in place would re-open the confirmation
3956
+ * on every later navigation, and would carry an order id into browser history
3957
+ * and any Referer this page later sends. `status` is only ever removed
3958
+ * alongside an `order` we actually consumed, so a host page that uses a
3959
+ * `status` parameter of its own keeps it.
3960
+ */
3961
+ resumeHostedOrder() {
3962
+ if (this.checkoutMode !== "hosted" || typeof location === "undefined") return;
3963
+ const params = new URLSearchParams(location.search);
3964
+ const orderId = params.get("order");
3965
+ if (!orderId) return;
3966
+ const status = params.get("status");
3967
+ params.delete("order");
3968
+ params.delete("status");
3969
+ try {
3970
+ const query = params.toString();
3971
+ history.replaceState(history.state, "", `${location.pathname}${query ? `?${query}` : ""}${location.hash}`);
3972
+ } catch {
3973
+ }
3974
+ if (status !== "success") return;
3975
+ void this.openCheckoutPanel({ kind: "resume", orderId });
3976
+ }
3512
3977
  /**
3513
3978
  * Move layout-dependent chrome between its wide dock (map regions / zoom
3514
3979
  * column) and its narrow dock (the sheet's consolidated Filters row), and
@@ -3534,27 +3999,27 @@ var SeatPicker = class _SeatPicker {
3534
3999
  }
3535
4000
  /** The "Need more time?" prompt shown in the hold's final EXTEND_PROMPT_MS. */
3536
4001
  buildExtendPrompt() {
3537
- const el = document.createElement("div");
3538
- el.className = "sl-extend";
3539
- el.setAttribute("role", "status");
3540
- el.innerHTML = `<span class="sl-extend-txt" data-ref="extendTxt"></span><button type="button" class="sl-extend-btn" data-ref="extendBtn"></button>`;
3541
- (this.regions["bottom-center"] ?? this.els.map).appendChild(el);
3542
- this.extendEl = el;
3543
- this.els.extendTxt = el.querySelector('[data-ref="extendTxt"]');
3544
- this.els.extendBtn = el.querySelector('[data-ref="extendBtn"]');
4002
+ const el2 = document.createElement("div");
4003
+ el2.className = "sl-extend";
4004
+ el2.setAttribute("role", "status");
4005
+ el2.innerHTML = `<span class="sl-extend-txt" data-ref="extendTxt"></span><button type="button" class="sl-extend-btn" data-ref="extendBtn"></button>`;
4006
+ (this.regions["bottom-center"] ?? this.els.map).appendChild(el2);
4007
+ this.extendEl = el2;
4008
+ this.els.extendTxt = el2.querySelector('[data-ref="extendTxt"]');
4009
+ this.els.extendBtn = el2.querySelector('[data-ref="extendBtn"]');
3545
4010
  this.els.extendBtn.textContent = "Add time";
3546
4011
  this.els.extendBtn.addEventListener("click", () => void this.handleExtend());
3547
4012
  }
3548
4013
  /** Success overlay + onBooked fire when the held seats settle to booked. */
3549
4014
  buildBookedOverlay() {
3550
- const el = document.createElement("div");
3551
- el.className = "sl-booked";
3552
- el.setAttribute("role", "status");
3553
- el.setAttribute("aria-live", "polite");
3554
- el.innerHTML = `<div class="sl-booked-badge"><svg viewBox="0 0 24 24"><path d="M20 6L9 17l-5-5"/></svg></div><div class="sl-booked-title">You're all set</div><div class="sl-booked-sub" data-ref="bookedSub"></div>`;
3555
- this.root.appendChild(el);
3556
- this.bookedEl = el;
3557
- this.els.bookedSub = el.querySelector('[data-ref="bookedSub"]');
4015
+ const el2 = document.createElement("div");
4016
+ el2.className = "sl-booked";
4017
+ el2.setAttribute("role", "status");
4018
+ el2.setAttribute("aria-live", "polite");
4019
+ el2.innerHTML = `<div class="sl-booked-badge"><svg viewBox="0 0 24 24"><path d="M20 6L9 17l-5-5"/></svg></div><div class="sl-booked-title">You're all set</div><div class="sl-booked-sub" data-ref="bookedSub"></div>`;
4020
+ this.root.appendChild(el2);
4021
+ this.bookedEl = el2;
4022
+ this.els.bookedSub = el2.querySelector('[data-ref="bookedSub"]');
3558
4023
  }
3559
4024
  /**
3560
4025
  * Localized string with a literal fallback. `t()` returns the key itself for
@@ -3568,13 +4033,13 @@ var SeatPicker = class _SeatPicker {
3568
4033
  /** Sold-out overlay — centered over the map, disabled waitlist stub (Gap 2). */
3569
4034
  buildSoldoutOverlay() {
3570
4035
  if (!this.els.map) return;
3571
- const el = document.createElement("div");
3572
- el.className = "sl-soldout";
3573
- el.setAttribute("role", "status");
4036
+ const el2 = document.createElement("div");
4037
+ el2.className = "sl-soldout";
4038
+ el2.setAttribute("role", "status");
3574
4039
  const name = (this.controller.doc?.theme?.brandName ?? this.opts.theme?.brandName ?? this.els.name?.textContent ?? this.tf("picker.soldOutEyebrow", "This event")).toUpperCase();
3575
- 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>`;
3576
- this.els.map.appendChild(el);
3577
- this.soldoutEl = el;
4040
+ el2.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>`;
4041
+ this.els.map.appendChild(el2);
4042
+ this.soldoutEl = el2;
3578
4043
  }
3579
4044
  /**
3580
4045
  * Recompute the sold-out state on every price/availability sync. Sold-out ⇔
@@ -3631,10 +4096,10 @@ var SeatPicker = class _SeatPicker {
3631
4096
  if (this.badgeHidden(chartTheme)) return;
3632
4097
  const foot = this.els.foot;
3633
4098
  if (!foot) return;
3634
- const el = document.createElement("div");
3635
- el.className = "sl-powered";
3636
- el.innerHTML = `<span class="sl-powered-mark" aria-hidden="true">` + SEATLAYER_ATTRIBUTION_MARK_SVG + `</span><span>${this.tf("picker.poweredBy", "Powered by SeatLayer")}</span>`;
3637
- foot.appendChild(el);
4099
+ const el2 = document.createElement("div");
4100
+ el2.className = "sl-powered";
4101
+ el2.innerHTML = `<span class="sl-powered-mark" aria-hidden="true">` + SEATLAYER_ATTRIBUTION_MARK_SVG + `</span><span>${this.tf("picker.poweredBy", "Powered by SeatLayer")}</span>`;
4102
+ foot.appendChild(el2);
3638
4103
  }
3639
4104
  // ---- Feature 6: chrome anchor regions -------------------------------------
3640
4105
  /**
@@ -3648,11 +4113,11 @@ var SeatPicker = class _SeatPicker {
3648
4113
  if (!this.els.map) return;
3649
4114
  const REGIONS = ["top-left", "top-center", "top-right", "left-rail", "bottom-left", "bottom-center", "bottom-right"];
3650
4115
  for (const region of REGIONS) {
3651
- const el = document.createElement("div");
3652
- el.className = "sl-anchor";
3653
- el.dataset.region = region;
3654
- this.els.map.appendChild(el);
3655
- this.regions[region] = el;
4116
+ const el2 = document.createElement("div");
4117
+ el2.className = "sl-anchor";
4118
+ el2.dataset.region = region;
4119
+ this.els.map.appendChild(el2);
4120
+ this.regions[region] = el2;
3656
4121
  }
3657
4122
  }
3658
4123
  // ---- F3 minimap -----------------------------------------------------------
@@ -3672,12 +4137,12 @@ var SeatPicker = class _SeatPicker {
3672
4137
  this.motionTimers.add(timer);
3673
4138
  }
3674
4139
  /** Restart one finite CSS animation without leaving a permanent state class. */
3675
- animateOnce(el, className, duration = 600) {
3676
- if (!el || this.reducedMotion()) return;
3677
- el.classList.remove(className);
3678
- void el.offsetWidth;
3679
- el.classList.add(className);
3680
- this.scheduleMotion(() => el.classList.remove(className), duration);
4140
+ animateOnce(el2, className, duration = 600) {
4141
+ if (!el2 || this.reducedMotion()) return;
4142
+ el2.classList.remove(className);
4143
+ void el2.offsetWidth;
4144
+ el2.classList.add(className);
4145
+ this.scheduleMotion(() => el2.classList.remove(className), duration);
3681
4146
  }
3682
4147
  /** Selection feedback belongs on the selected seat, not across the whole map. */
3683
4148
  flashPickedSeat(id) {
@@ -4331,13 +4796,13 @@ var SeatPicker = class _SeatPicker {
4331
4796
  const cat = this.controller.doc?.categories.find((candidate) => candidate.key === table.categoryKey);
4332
4797
  const variable = table.bookingMode === "variable";
4333
4798
  const typeWord = this.rowTypeWord(table);
4334
- const el = document.createElement("div");
4335
- el.className = "sl-table-scrim";
4336
- el.innerHTML = `<section class="sl-table-dialog" role="dialog" aria-modal="true" aria-labelledby="sl-table-title" aria-describedby="sl-table-copy"><div class="sl-table-head"><div class="sl-table-eyebrow">${esc3(variable ? `Flexible party \xB7 ${typeWord}` : `Whole ${typeWord}`)}</div><h2 class="sl-table-title" id="sl-table-title">${esc3(table.displayLabel ?? table.label)}</h2><p class="sl-table-copy" id="sl-table-copy">${variable ? `Choose how many guests will sit together. This table is held exclusively for your party.` : `All ${table.capacity} places are booked together as one exclusive table.`}</p></div><div class="sl-table-body"><div class="sl-table-summary"><span>${esc3(cat?.label ?? table.categoryKey)}</span><b data-table-unit>${this.money(this.paidPrice(table.categoryKey, table.tierId ?? null, table.price))} per guest</b><span class="muted">Table capacity</span><span>${table.capacity} guests</span><span class="muted">Total</span><b data-table-total></b></div>` + (variable ? `<label class="sl-table-qtylabel" id="sl-table-qty-label">Number of guests</label><div class="sl-table-stepper" role="group" aria-labelledby="sl-table-qty-label"><button type="button" data-table-step="-1" aria-label="Fewer guests">\u2212</button><output aria-live="polite" data-table-qty>${table.quantity}</output><button type="button" data-table-step="1" aria-label="More guests">+</button></div><div class="sl-table-range">Choose ${table.minOccupancy}\u2013${table.maxOccupancy} guests</div>` : `<input type="hidden" data-table-qty value="${table.capacity}">`) + `<div class="sl-table-actions"><button type="button" class="sl-table-cancel">Cancel</button><button type="button" class="sl-table-confirm">${held ? "Update table" : variable ? "Select table" : "Select whole table"}</button></div></div></section>`;
4337
- this.root.appendChild(el);
4338
- this.tableDialogEl = el;
4799
+ const el2 = document.createElement("div");
4800
+ el2.className = "sl-table-scrim";
4801
+ el2.innerHTML = `<section class="sl-table-dialog" role="dialog" aria-modal="true" aria-labelledby="sl-table-title" aria-describedby="sl-table-copy"><div class="sl-table-head"><div class="sl-table-eyebrow">${esc3(variable ? `Flexible party \xB7 ${typeWord}` : `Whole ${typeWord}`)}</div><h2 class="sl-table-title" id="sl-table-title">${esc3(table.displayLabel ?? table.label)}</h2><p class="sl-table-copy" id="sl-table-copy">${variable ? `Choose how many guests will sit together. This table is held exclusively for your party.` : `All ${table.capacity} places are booked together as one exclusive table.`}</p></div><div class="sl-table-body"><div class="sl-table-summary"><span>${esc3(cat?.label ?? table.categoryKey)}</span><b data-table-unit>${this.money(this.paidPrice(table.categoryKey, table.tierId ?? null, table.price))} per guest</b><span class="muted">Table capacity</span><span>${table.capacity} guests</span><span class="muted">Total</span><b data-table-total></b></div>` + (variable ? `<label class="sl-table-qtylabel" id="sl-table-qty-label">Number of guests</label><div class="sl-table-stepper" role="group" aria-labelledby="sl-table-qty-label"><button type="button" data-table-step="-1" aria-label="Fewer guests">\u2212</button><output aria-live="polite" data-table-qty>${table.quantity}</output><button type="button" data-table-step="1" aria-label="More guests">+</button></div><div class="sl-table-range">Choose ${table.minOccupancy}\u2013${table.maxOccupancy} guests</div>` : `<input type="hidden" data-table-qty value="${table.capacity}">`) + `<div class="sl-table-actions"><button type="button" class="sl-table-cancel">Cancel</button><button type="button" class="sl-table-confirm">${held ? "Update table" : variable ? "Select table" : "Select whole table"}</button></div></div></section>`;
4802
+ this.root.appendChild(el2);
4803
+ this.tableDialogEl = el2;
4339
4804
  this.renderTableDialogState();
4340
- el.querySelectorAll("[data-table-step]").forEach((button) => {
4805
+ el2.querySelectorAll("[data-table-step]").forEach((button) => {
4341
4806
  button.addEventListener("click", () => {
4342
4807
  if (!this.tableDialog) return;
4343
4808
  const next = Math.max(
@@ -4348,14 +4813,14 @@ var SeatPicker = class _SeatPicker {
4348
4813
  this.renderTableDialogState();
4349
4814
  });
4350
4815
  });
4351
- el.querySelector(".sl-table-cancel").addEventListener("click", () => this.cancelTableDialog());
4352
- el.querySelector(".sl-table-confirm").addEventListener("click", () => void this.confirmTableDialog());
4353
- el.addEventListener("mousedown", (event) => {
4354
- if (event.target === el) this.cancelTableDialog();
4816
+ el2.querySelector(".sl-table-cancel").addEventListener("click", () => this.cancelTableDialog());
4817
+ el2.querySelector(".sl-table-confirm").addEventListener("click", () => void this.confirmTableDialog());
4818
+ el2.addEventListener("mousedown", (event) => {
4819
+ if (event.target === el2) this.cancelTableDialog();
4355
4820
  });
4356
- el.addEventListener("keydown", (event) => {
4821
+ el2.addEventListener("keydown", (event) => {
4357
4822
  if (event.key !== "Tab") return;
4358
- const focusable = [...el.querySelectorAll('button:not(:disabled),[tabindex]:not([tabindex="-1"])')];
4823
+ const focusable = [...el2.querySelectorAll('button:not(:disabled),[tabindex]:not([tabindex="-1"])')];
4359
4824
  if (!focusable.length) return;
4360
4825
  const first = focusable[0];
4361
4826
  const last = focusable[focusable.length - 1];
@@ -4367,22 +4832,22 @@ var SeatPicker = class _SeatPicker {
4367
4832
  first.focus();
4368
4833
  }
4369
4834
  });
4370
- requestAnimationFrame(() => el.querySelector(variable ? '[data-table-step="-1"]' : ".sl-table-confirm")?.focus());
4835
+ requestAnimationFrame(() => el2.querySelector(variable ? '[data-table-step="-1"]' : ".sl-table-confirm")?.focus());
4371
4836
  }
4372
4837
  renderTableDialogState() {
4373
4838
  const table = this.tableDialog;
4374
- const el = this.tableDialogEl;
4375
- if (!table || !el) return;
4376
- const output = el.querySelector("output[data-table-qty]");
4839
+ const el2 = this.tableDialogEl;
4840
+ if (!table || !el2) return;
4841
+ const output = el2.querySelector("output[data-table-qty]");
4377
4842
  if (output) output.value = String(table.quantity);
4378
- const hidden = el.querySelector("input[data-table-qty]");
4843
+ const hidden = el2.querySelector("input[data-table-qty]");
4379
4844
  if (hidden) hidden.value = String(table.quantity);
4380
- el.querySelectorAll("[data-table-step]").forEach((button) => {
4845
+ el2.querySelectorAll("[data-table-step]").forEach((button) => {
4381
4846
  const delta = Number(button.dataset.tableStep);
4382
4847
  button.disabled = delta < 0 ? table.quantity <= table.minOccupancy : table.quantity >= table.maxOccupancy;
4383
4848
  });
4384
4849
  const unit = this.paidPrice(table.categoryKey, table.tierId ?? null, table.price);
4385
- const total = el.querySelector("[data-table-total]");
4850
+ const total = el2.querySelector("[data-table-total]");
4386
4851
  if (total) total.textContent = this.money(unit * table.quantity);
4387
4852
  }
4388
4853
  async confirmTableDialog() {
@@ -4478,18 +4943,18 @@ var SeatPicker = class _SeatPicker {
4478
4943
  details?.rowLabel || details?.objectType === "booth" ? `<div class="sl-confirm-field"><span class="sl-confirm-key">${safe(this.rowTypeWord(details))}</span><span class="sl-confirm-value">${safe(this.rowShort(details) ?? details?.displayLabel ?? seat.displayLabel ?? seat.label)}</span></div>` : "",
4479
4944
  details?.objectType !== "booth" ? `<div class="sl-confirm-field"><span class="sl-confirm-key">Seat</span><span class="sl-confirm-value">${safe(details?.seatNumber ?? details?.displayLabel ?? seat.displayLabel ?? seat.label)}</span></div>` : ""
4480
4945
  ].filter(Boolean).join("");
4481
- const el = document.createElement("div");
4482
- el.className = "sl-confirm";
4483
- el.setAttribute("role", "dialog");
4484
- el.setAttribute("aria-modal", "true");
4485
- el.setAttribute("aria-label", `Confirm seat ${seat.label}`);
4486
- el.style.setProperty("--sl-cat", cat?.color ?? "#6e7bff");
4487
- el.innerHTML = `<div class="sl-confirm-grid">` + identityFields + `</div><div class="sl-confirm-cat"><span class="sl-dot" style="background:${cat?.color ?? "#6e7bff"}"></span><span class="sl-confirm-cat-name">${safe(details?.categoryLabel ?? cat?.label ?? seat.categoryKey)}</span>` + (price != null ? `<span class="sl-confirm-price">${this.money(price)}</span>` : "") + `</div><div class="sl-confirm-body">` + this.wheelchairConfirmHtml(details?.wheelchairSpaceType) + this.commercialConfirmHtml(seat.commercial) + (this.seatViewEnabled() ? this.confirmThumbHtml(seat) : "") + this.see3dConfirmHtml() + `<div class="sl-confirm-row"><button type="button" class="sl-confirm-cancel">Cancel</button><button type="button" class="sl-confirm-add"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M5 12.5l4 4L19 7"/></svg>Select</button></div></div>`;
4488
- this.els.map.appendChild(el);
4489
- this.confirmEl = el;
4946
+ const el2 = document.createElement("div");
4947
+ el2.className = "sl-confirm";
4948
+ el2.setAttribute("role", "dialog");
4949
+ el2.setAttribute("aria-modal", "true");
4950
+ el2.setAttribute("aria-label", `Confirm seat ${seat.label}`);
4951
+ el2.style.setProperty("--sl-cat", cat?.color ?? "#6e7bff");
4952
+ el2.innerHTML = `<div class="sl-confirm-grid">` + identityFields + `</div><div class="sl-confirm-cat"><span class="sl-dot" style="background:${cat?.color ?? "#6e7bff"}"></span><span class="sl-confirm-cat-name">${safe(details?.categoryLabel ?? cat?.label ?? seat.categoryKey)}</span>` + (price != null ? `<span class="sl-confirm-price">${this.money(price)}</span>` : "") + `</div><div class="sl-confirm-body">` + this.wheelchairConfirmHtml(details?.wheelchairSpaceType) + this.commercialConfirmHtml(seat.commercial) + (this.seatViewEnabled() ? this.confirmThumbHtml(seat) : "") + this.see3dConfirmHtml() + `<div class="sl-confirm-row"><button type="button" class="sl-confirm-cancel">Cancel</button><button type="button" class="sl-confirm-add"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M5 12.5l4 4L19 7"/></svg>Select</button></div></div>`;
4953
+ this.els.map.appendChild(el2);
4954
+ this.confirmEl = el2;
4490
4955
  this.reanchorConfirm();
4491
- el.querySelector(".sl-confirm-view")?.addEventListener("click", () => this.openSeatView(seat));
4492
- el.querySelector(".sl-confirm-3d")?.addEventListener("click", () => {
4956
+ el2.querySelector(".sl-confirm-view")?.addEventListener("click", () => void this.openSeatView(seat));
4957
+ el2.querySelector(".sl-confirm-3d")?.addEventListener("click", () => {
4493
4958
  if (this.buyerView === "venue3d") {
4494
4959
  void this.view3dHandle?.flyToSeat(seat.id);
4495
4960
  } else {
@@ -4498,9 +4963,9 @@ var SeatPicker = class _SeatPicker {
4498
4963
  void this.enter3d(seat.id);
4499
4964
  }
4500
4965
  });
4501
- el.querySelector(".sl-confirm-add").addEventListener("click", () => this.commitConfirm());
4502
- el.querySelector(".sl-confirm-cancel").addEventListener("click", () => this.cancelConfirm());
4503
- requestAnimationFrame(() => el.querySelector(".sl-confirm-add")?.focus());
4966
+ el2.querySelector(".sl-confirm-add").addEventListener("click", () => this.commitConfirm());
4967
+ el2.querySelector(".sl-confirm-cancel").addEventListener("click", () => this.cancelConfirm());
4968
+ requestAnimationFrame(() => el2.querySelector(".sl-confirm-add")?.focus());
4504
4969
  }
4505
4970
  reanchorConfirm() {
4506
4971
  if (!this.confirmEl || !this.confirmSeat) return;
@@ -4559,10 +5024,13 @@ var SeatPicker = class _SeatPicker {
4559
5024
  * uploaded photo (seat.viewUrl) when present, else a panorama generated from
4560
5025
  * the chart geometry — the stage placed at this seat's true bearing + size.
4561
5026
  * Zero extra dependencies: an equirectangular image panned with `repeat-x`.
5027
+ *
5028
+ * Async only because the generator is a lazy chunk (see `loadPanorama`); an
5029
+ * organizer photo needs no generator and never waits on it. The two callers
5030
+ * are click handlers, so nothing observes the promise.
4562
5031
  */
4563
- openSeatView(seat) {
5032
+ async openSeatView(seat) {
4564
5033
  if (!this.root || !this.seatViewEnabled()) return;
4565
- this.closeSeatView();
4566
5034
  const doc = this.controller.doc;
4567
5035
  const activeId = this.controller.getActiveFloorId();
4568
5036
  const focal = seat.focalPoint ?? doc?.floors?.find((f) => f.id === activeId)?.focalPoint ?? doc?.focalPoint ?? { x: 0, y: 0 };
@@ -4574,18 +5042,27 @@ var SeatPicker = class _SeatPicker {
4574
5042
  caption = (0, import_core2.t)("picker.panorama360");
4575
5043
  real = true;
4576
5044
  } else {
4577
- const pano2 = (0, import_core2.generateSeatPanorama)(seat, focal, this.allSeats());
5045
+ let pano2;
5046
+ try {
5047
+ const { generateSeatPanorama } = await loadPanorama();
5048
+ pano2 = generateSeatPanorama(seat, focal, this.allSeats());
5049
+ } catch (err) {
5050
+ this.opts.onError?.(err);
5051
+ return;
5052
+ }
5053
+ if (!this.root || !this.seatViewEnabled()) return;
4578
5054
  panoUrl = pano2.url;
4579
5055
  caption = (0, import_core2.t)("picker.illustrationCaption", { m: pano2.distanceM });
4580
5056
  }
4581
- const el = document.createElement("div");
4582
- el.className = "sl-view";
4583
- el.setAttribute("role", "dialog");
4584
- el.setAttribute("aria-label", (0, import_core2.t)("picker.viewFromSeat", { label: seat.label }));
4585
- el.innerHTML = `<div class="sl-view-head"><span class="sl-view-title">${(0, import_core2.t)("picker.viewFromSeat", { label: seat.label })}</span><span class="sl-view-cap">${caption}</span><button type="button" class="sl-view-x" aria-label="Close"><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><div class="sl-view-pano"><span class="sl-view-badge">${real ? (0, import_core2.t)("picker.real360") : (0, import_core2.t)("picker.preview")}</span><span class="sl-view-hint">Drag to look around \xB7 scroll to zoom</span></div>`;
4586
- this.root.appendChild(el);
4587
- this.viewEl = el;
4588
- const pano = el.querySelector(".sl-view-pano");
5057
+ this.closeSeatView();
5058
+ const el2 = document.createElement("div");
5059
+ el2.className = "sl-view";
5060
+ el2.setAttribute("role", "dialog");
5061
+ el2.setAttribute("aria-label", (0, import_core2.t)("picker.viewFromSeat", { label: seat.label }));
5062
+ el2.innerHTML = `<div class="sl-view-head"><span class="sl-view-title">${(0, import_core2.t)("picker.viewFromSeat", { label: seat.label })}</span><span class="sl-view-cap">${caption}</span><button type="button" class="sl-view-x" aria-label="Close"><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><div class="sl-view-pano"><span class="sl-view-badge">${real ? (0, import_core2.t)("picker.real360") : (0, import_core2.t)("picker.preview")}</span><span class="sl-view-hint">Drag to look around \xB7 scroll to zoom</span></div>`;
5063
+ this.root.appendChild(el2);
5064
+ this.viewEl = el2;
5065
+ const pano = el2.querySelector(".sl-view-pano");
4589
5066
  pano.style.backgroundImage = `url("${panoUrl}")`;
4590
5067
  const VFOV_DEG = 70;
4591
5068
  const MAX_PITCH_DEG = 35;
@@ -4637,7 +5114,7 @@ var SeatPicker = class _SeatPicker {
4637
5114
  pano.addEventListener("pointerup", onUp);
4638
5115
  pano.addEventListener("pointercancel", onUp);
4639
5116
  pano.addEventListener("wheel", onWheel, { passive: false });
4640
- const closeBtn = el.querySelector(".sl-view-x");
5117
+ const closeBtn = el2.querySelector(".sl-view-x");
4641
5118
  closeBtn.addEventListener("click", () => this.closeSeatView());
4642
5119
  const onKey = (e) => {
4643
5120
  if (e.key === "Escape") {
@@ -4645,7 +5122,7 @@ var SeatPicker = class _SeatPicker {
4645
5122
  this.closeSeatView();
4646
5123
  }
4647
5124
  };
4648
- el.addEventListener("keydown", onKey);
5125
+ el2.addEventListener("keydown", onKey);
4649
5126
  closeBtn.focus();
4650
5127
  this.viewCleanup = () => {
4651
5128
  pano.removeEventListener("pointerdown", onDown);
@@ -4653,7 +5130,7 @@ var SeatPicker = class _SeatPicker {
4653
5130
  pano.removeEventListener("pointerup", onUp);
4654
5131
  pano.removeEventListener("pointercancel", onUp);
4655
5132
  pano.removeEventListener("wheel", onWheel);
4656
- el.removeEventListener("keydown", onKey);
5133
+ el2.removeEventListener("keydown", onKey);
4657
5134
  };
4658
5135
  }
4659
5136
  closeSeatView() {
@@ -4943,7 +5420,7 @@ var SeatPicker = class _SeatPicker {
4943
5420
  this.els.tray.querySelectorAll(".sl-chip .view[data-view-label]").forEach((btn) => {
4944
5421
  btn.addEventListener("click", () => {
4945
5422
  const seat = this.controller.seatByLabel(btn.dataset.viewLabel);
4946
- if (seat) this.openSeatView(seat);
5423
+ if (seat) void this.openSeatView(seat);
4947
5424
  });
4948
5425
  });
4949
5426
  this.els.tray.querySelectorAll(".sl-chip[data-locate]").forEach((chip) => {
@@ -4964,8 +5441,8 @@ var SeatPicker = class _SeatPicker {
4964
5441
  });
4965
5442
  });
4966
5443
  if (this.salesClosed) {
4967
- this.els.tray.querySelectorAll(".sl-ba-go,[data-ba],[data-ba-cat],[data-ba-zone],[data-ba-replace],.sl-ga button").forEach((el) => {
4968
- el.disabled = true;
5444
+ this.els.tray.querySelectorAll(".sl-ba-go,[data-ba],[data-ba-cat],[data-ba-zone],[data-ba-replace],.sl-ga button").forEach((el2) => {
5445
+ el2.disabled = true;
4969
5446
  });
4970
5447
  }
4971
5448
  const gaTotal = this.pendingGATotal(gaAreas);
@@ -5086,7 +5563,7 @@ var SeatPicker = class _SeatPicker {
5086
5563
  const seats = this.hold.seats ?? committed;
5087
5564
  this.handedOff = true;
5088
5565
  this.setCtaPhase("checkout");
5089
- this.opts.onCheckout?.(this.hold, seats, this.buildHandoff(this.hold));
5566
+ this.checkoutHandoff(this.hold, seats);
5090
5567
  return;
5091
5568
  }
5092
5569
  this.holdingLabels = new Set(committed.map((seat) => seat.label));
@@ -5115,7 +5592,7 @@ var SeatPicker = class _SeatPicker {
5115
5592
  this.flashHeldSeats(hold);
5116
5593
  this.setCtaPhase("checkout");
5117
5594
  this.emitHoldChange();
5118
- this.opts.onCheckout?.(hold, hold.seats ?? chosenSeats, this.buildHandoff(hold));
5595
+ this.checkoutHandoff(hold, hold.seats ?? chosenSeats);
5119
5596
  } catch (err) {
5120
5597
  this.opts.onError?.(err);
5121
5598
  const problem = err;
@@ -5220,6 +5697,109 @@ var SeatPicker = class _SeatPicker {
5220
5697
  this.bookedEl?.classList.add("on");
5221
5698
  this.opts.onBooked?.(handoff);
5222
5699
  }
5700
+ /**
5701
+ * The seats are held. Send the buyer wherever this picker's `checkout` option
5702
+ * says they go.
5703
+ *
5704
+ * The default branch is the literal call that stood here before hosted
5705
+ * checkout existed, unchanged, so nothing about an existing integration moves.
5706
+ */
5707
+ checkoutHandoff(hold, seats) {
5708
+ if (this.checkoutMode === "hosted") {
5709
+ void this.startHostedCheckout(hold, seats);
5710
+ return;
5711
+ }
5712
+ this.opts.onCheckout?.(hold, seats, this.buildHandoff(hold));
5713
+ }
5714
+ /**
5715
+ * Take the money ourselves, through the organizer's own gateway.
5716
+ *
5717
+ * Order of operations matters: ASK FIRST, load second. `payment-options` is
5718
+ * already in flight from render, and its answer decides whether any payment
5719
+ * code is fetched at all — an event that cannot charge never downloads the
5720
+ * card that would have charged it.
5721
+ *
5722
+ * An empty list is not a failure and never dead-ends the buyer. It routes them
5723
+ * to whatever the host has: `onCheckoutUnavailable` (with the server's reason,
5724
+ * so the host can say the right one of three very different sentences), then
5725
+ * `onCheckout` with the ordinary handoff. A host that supplied neither gets
5726
+ * the widget's own honest card instead of a press that did nothing.
5727
+ */
5728
+ async startHostedCheckout(hold, seats) {
5729
+ const handoff = this.buildHandoff(hold);
5730
+ let options = null;
5731
+ try {
5732
+ options = await (this.paymentOptions ??= this.pubApi.paymentOptions(this.opts.event));
5733
+ } catch (err) {
5734
+ this.opts.onError?.(err);
5735
+ }
5736
+ if (this.destroyed) return;
5737
+ const provider = options?.providers?.[0];
5738
+ if (!provider) {
5739
+ const reason = paymentsOffReason(options?.reason);
5740
+ const handled = !!this.opts.onCheckoutUnavailable || !!this.opts.onCheckout;
5741
+ this.opts.onCheckoutUnavailable?.({ reason, handoff });
5742
+ this.opts.onCheckout?.(hold, seats, handoff);
5743
+ if (!handled) {
5744
+ void this.openCheckoutPanel({
5745
+ kind: "unavailable",
5746
+ reason,
5747
+ seatCount: handoff.lineItems.reduce((sum, item) => sum + item.quantity, 0)
5748
+ });
5749
+ }
5750
+ return;
5751
+ }
5752
+ await this.openCheckoutPanel({
5753
+ kind: "pay",
5754
+ provider,
5755
+ order: {
5756
+ holdId: handoff.holdId,
5757
+ expiresAt: handoff.expiresAt,
5758
+ currency: handoff.currency,
5759
+ total: handoff.total,
5760
+ labels: handoff.lineItems.map((item) => item.displayLabel ?? item.label)
5761
+ }
5762
+ });
5763
+ }
5764
+ /**
5765
+ * Fetch the checkout chunk and put its card over the map.
5766
+ *
5767
+ * Every failure here lands the buyer back on a map with their seats still
5768
+ * held, which is a place they can act from — a blocked chunk request must not
5769
+ * leave them staring at a CTA that no longer does anything.
5770
+ */
5771
+ async openCheckoutPanel(state) {
5772
+ let mountCheckout2;
5773
+ try {
5774
+ ({ mountCheckout: mountCheckout2 } = await loadHostedCheckout());
5775
+ } catch (err) {
5776
+ this.opts.onError?.(err);
5777
+ this.toast("Checkout could not be opened. Your seats are still held \u2014 please try again.", "error");
5778
+ this.setCtaPhase("idle");
5779
+ return;
5780
+ }
5781
+ if (this.destroyed || !this.root) return;
5782
+ this.closeCheckoutPanel();
5783
+ this.checkoutPanel = mountCheckout2({
5784
+ root: this.root,
5785
+ state,
5786
+ startSession: (input) => this.pubApi.startCheckout(this.opts.event, input),
5787
+ orderStatus: (orderId) => this.pubApi.orderStatus(orderId),
5788
+ onCancel: () => {
5789
+ this.checkoutPanel = null;
5790
+ if (!this.hold) this.setCtaPhase("idle");
5791
+ },
5792
+ onConfirmed: (order) => {
5793
+ this.opts.onOrderConfirmed?.(order);
5794
+ void this.controller.refresh();
5795
+ },
5796
+ onError: (err) => this.opts.onError?.(err)
5797
+ });
5798
+ }
5799
+ closeCheckoutPanel() {
5800
+ this.checkoutPanel?.destroy();
5801
+ this.checkoutPanel = null;
5802
+ }
5223
5803
  /** Assemble the stable {@link CheckoutHandoff} from a hold's server line items. */
5224
5804
  buildHandoff(hold) {
5225
5805
  const items = hold.items ?? [];
@@ -5251,28 +5831,28 @@ var SeatPicker = class _SeatPicker {
5251
5831
  );
5252
5832
  }
5253
5833
  toast(msg, tone = "neutral", action) {
5254
- const el = this.els.toast;
5255
- if (!el) return;
5256
- el.replaceChildren();
5834
+ const el2 = this.els.toast;
5835
+ if (!el2) return;
5836
+ el2.replaceChildren();
5257
5837
  const copy = document.createElement("span");
5258
5838
  copy.textContent = msg;
5259
- el.appendChild(copy);
5260
- el.classList.toggle("has-action", !!action);
5839
+ el2.appendChild(copy);
5840
+ el2.classList.toggle("has-action", !!action);
5261
5841
  if (action) {
5262
5842
  const button = document.createElement("button");
5263
5843
  button.type = "button";
5264
5844
  button.className = "sl-toast-action";
5265
5845
  button.textContent = action.label;
5266
5846
  button.addEventListener("click", action.onClick, { once: true });
5267
- el.appendChild(button);
5847
+ el2.appendChild(button);
5268
5848
  }
5269
- el.dataset.tone = tone;
5270
- el.classList.add("on");
5849
+ el2.dataset.tone = tone;
5850
+ el2.classList.add("on");
5271
5851
  if (this.toastTimer) clearTimeout(this.toastTimer);
5272
5852
  this.toastTimer = setTimeout(() => {
5273
- el.classList.remove("on");
5274
- el.classList.remove("has-action");
5275
- el.dataset.tone = "neutral";
5853
+ el2.classList.remove("on");
5854
+ el2.classList.remove("has-action");
5855
+ el2.dataset.tone = "neutral";
5276
5856
  }, 4200);
5277
5857
  }
5278
5858
  placeTooltip() {
@@ -5454,7 +6034,7 @@ var SeatPicker = class _SeatPicker {
5454
6034
  }
5455
6035
  /** Build the view-from-seat panorama the cinematic dissolves into — reuses the
5456
6036
  * exact input path as the 2D `openSeatView` (organizer photo, else generated). */
5457
- seatViewFor3d(seatId) {
6037
+ async seatViewFor3d(seatId) {
5458
6038
  const seat = this.allSeats().find((s) => s.id === seatId);
5459
6039
  if (!seat) return null;
5460
6040
  if (seat.viewUrl) return { url: seat.viewUrl };
@@ -5463,7 +6043,8 @@ var SeatPicker = class _SeatPicker {
5463
6043
  const activeId = this.controller.getActiveFloorId();
5464
6044
  const focal = seat.focalPoint ?? doc.floors?.find((f) => f.id === activeId)?.focalPoint ?? doc.focalPoint ?? { x: 0, y: 0 };
5465
6045
  try {
5466
- return { url: (0, import_core2.generateSeatPanorama)(seat, focal, this.allSeats()).url };
6046
+ const { generateSeatPanorama } = await loadPanorama();
6047
+ return { url: generateSeatPanorama(seat, focal, this.allSeats()).url };
5467
6048
  } catch {
5468
6049
  return null;
5469
6050
  }
@@ -5602,9 +6183,10 @@ var SeatPicker = class _SeatPicker {
5602
6183
  // in orbit instead of dissolving into an empty overlay.
5603
6184
  getSeatView: (id) => new Promise((resolve, reject) => {
5604
6185
  const run = () => {
5605
- const view = this.seatViewFor3d(id);
5606
- if (view) resolve(view);
5607
- else reject(new Error("seat_view_unavailable"));
6186
+ void this.seatViewFor3d(id).then((view) => {
6187
+ if (view) resolve(view);
6188
+ else reject(new Error("seat_view_unavailable"));
6189
+ });
5608
6190
  };
5609
6191
  const ric = globalThis.requestIdleCallback;
5610
6192
  if (typeof ric === "function") ric(run, { timeout: 1500 });
@@ -5882,6 +6464,7 @@ var SeatPicker = class _SeatPicker {
5882
6464
  this.closeConfirm();
5883
6465
  this.dismissTableDialog(false);
5884
6466
  this.closeSeatView();
6467
+ this.closeCheckoutPanel();
5885
6468
  this.exit3d();
5886
6469
  this.stopHoldTimer();
5887
6470
  if (this.toastTimer) clearTimeout(this.toastTimer);
@@ -6598,7 +7181,14 @@ var ManageApi = class {
6598
7181
  var POLL_MS = 1e4;
6599
7182
  var MAX_FLAGS = 8;
6600
7183
  var SEAT_LIST_PAGE = 300;
6601
- var CHANNELS_CSS = `
7184
+ var PREVIEW_ELIGIBLE_FILL = "#6e7bff";
7185
+ var PREVIEW_ELIGIBLE_STROKE = "#b9c0ff";
7186
+ var PREVIEW_UNAVAILABLE_FILL = "#303846";
7187
+ var PREVIEW_UNAVAILABLE_STROKE = "#4b5669";
7188
+ var ALLOCATION_STROKE = "#101723";
7189
+ var CHANNELS_CSS = (
7190
+ /* @sl-css */
7191
+ `
6602
7192
  .slm{--slm-mo-instant:80ms;--slm-mo-quick:140ms;--slm-mo-base:200ms;--slm-mo-slow:320ms;--slm-mo-ambient:2000ms;
6603
7193
  --slm-mo-out:cubic-bezier(.2,.8,.2,1);--slm-mo-in-out:cubic-bezier(.4,0,.2,1);--slm-mo-exit:cubic-bezier(.4,0,1,1);
6604
7194
  --slm-mo-spring:cubic-bezier(.34,1.3,.64,1)}
@@ -6611,6 +7201,8 @@ var CHANNELS_CSS = `
6611
7201
  background:rgba(14,16,23,.88);border:1px solid var(--slm-line);font-size:10px;font-weight:800;letter-spacing:.04em;
6612
7202
  transform:translate(-50%,-50%);white-space:nowrap}
6613
7203
  .slm-ch-flag .mk{width:14px;height:14px;border-radius:4px;display:grid;place-items:center;font-size:8.5px;font-weight:800;color:#0e1017}
7204
+ .slm-ch-section-target{position:absolute;pointer-events:auto;padding:0;border:0;border-radius:8px;background:transparent;cursor:zoom-in}
7205
+ .slm-ch-section-target:focus-visible{outline:2px solid var(--slm-accent);outline-offset:-3px;background:color-mix(in srgb,var(--slm-accent) 12%,transparent)}
6614
7206
 
6615
7207
  /* preview banner \u2014 raised with the organizer chrome dim, as one transition */
6616
7208
  .slm-ch-banner{position:absolute;left:50%;top:14px;z-index:6;display:flex;align-items:center;gap:9px;padding:8px 14px;
@@ -6630,7 +7222,7 @@ var CHANNELS_CSS = `
6630
7222
  transition:transform var(--slm-mo-slow) var(--slm-mo-out),opacity var(--slm-mo-base) var(--slm-mo-out)}
6631
7223
  .slm-ch-staged.on{transform:none;opacity:1}
6632
7224
  .slm-ch-staged.done{background:rgba(31,122,77,.96);border-color:#1f7a4d}
6633
- .slm-ch-staged.shake{animation:slm-ch-shake 320ms var(--slm-mo-in-out) 2}
7225
+ .slm-ch-staged.shake{animation:slm-ch-shake var(--slm-mo-slow) var(--slm-mo-in-out) 2}
6634
7226
  .slm-ch-staged b{font-variant-numeric:tabular-nums}
6635
7227
  .slm-ch-staged .grow{flex:1}
6636
7228
  .slm-ch-staged .go{padding:9px 16px;min-height:44px;display:inline-flex;align-items:center;border-radius:9px;
@@ -6647,6 +7239,11 @@ var CHANNELS_CSS = `
6647
7239
  .slm-ch-viewseg button{flex:1;padding:6px 8px;min-height:34px;border-radius:7px;font-size:11px;font-weight:800;color:var(--slm-muted)}
6648
7240
  .slm-ch-viewseg button.on{background:var(--slm-accent);color:var(--slm-accent-ink)}
6649
7241
  .slm-ch-viewseg button:disabled{opacity:.5;cursor:not-allowed}
7242
+ .slm-ch-mapnav{margin:-2px 0 12px;padding:10px;border:1px solid var(--slm-line);border-radius:10px;background:var(--slm-surface)}
7243
+ .slm-ch-mapnav-head{display:flex;align-items:center;justify-content:space-between;gap:8px;font-size:11px;font-weight:800;letter-spacing:.08em;text-transform:uppercase;color:var(--slm-muted)}
7244
+ .slm-ch-mapnav-head button{color:var(--slm-accent);font-size:11px;font-weight:800;letter-spacing:0;text-transform:none;min-height:30px}
7245
+ .slm-ch-mapnav .slm-ch-viewseg{margin:8px 0 5px}
7246
+ .slm-ch-mapnav p{margin:0;font-size:11px;line-height:1.45;color:var(--slm-muted)}
6650
7247
  .slm-ch-list{display:flex;flex-direction:column;gap:8px;margin-bottom:12px}
6651
7248
  .slm-ch-row{padding:10px 11px;border:1px solid var(--slm-line);border-radius:10px;background:var(--slm-surface);
6652
7249
  text-align:left;width:100%;display:block;transition:border-color var(--slm-mo-quick) var(--slm-mo-out)}
@@ -6667,7 +7264,7 @@ var CHANNELS_CSS = `
6667
7264
  font-variant-numeric:tabular-nums}
6668
7265
  .slm-ch-counts b{color:var(--slm-text);font-weight:800}
6669
7266
  .slm-ch-counts .free b{color:#5bd39b}
6670
- .slm-ch-counts b.bump{animation:slm-ch-bump .58s var(--slm-mo-spring)}
7267
+ .slm-ch-counts b.bump{animation:slm-ch-bump var(--slm-mo-base) var(--slm-mo-spring)}
6671
7268
  @keyframes slm-ch-bump{0%,100%{transform:none}35%{transform:translateY(-2px) scale(1.08)}}
6672
7269
  .slm-ch-access{margin-top:6px;font-size:10.5px;color:var(--slm-muted)}
6673
7270
  .slm-ch-more{flex:none;color:var(--slm-muted);font-weight:800;padding:0 4px;min-height:28px}
@@ -6677,12 +7274,13 @@ var CHANNELS_CSS = `
6677
7274
  font-weight:800;color:#0e1017}
6678
7275
  .slm-ch-selsrc-row b{min-width:30px;text-align:right;font-weight:800}
6679
7276
  .slm-ch-selsrc-row span{color:var(--slm-muted)}
6680
- .slm-ch-selnum.bump{animation:slm-ch-bump .58s var(--slm-mo-spring)}
7277
+ .slm-ch-selnum.bump{animation:slm-ch-bump var(--slm-mo-base) var(--slm-mo-spring)}
6681
7278
  .slm-ch-row2{display:flex;gap:8px;margin-top:8px}
6682
7279
  .slm-ch-row2 .slm-btn{flex:1;min-width:0}
6683
7280
  .slm-ch-alert{display:flex;align-items:flex-start;gap:9px;padding:11px 13px;border-radius:10px;font-size:12.5px;
6684
7281
  line-height:1.5;margin-bottom:12px}
6685
7282
  .slm-ch-alert.warn{background:rgba(244,183,64,.1);border:1px solid rgba(244,183,64,.4);color:#f4d58a}
7283
+ .slm-ch-alert.info{background:rgba(110,123,255,.12);border:1px solid rgba(110,123,255,.44);color:#c5cbff}
6686
7284
  .slm-ch-alert.err{background:rgba(229,72,77,.1);border:1px solid rgba(229,72,77,.45);color:#f1a4a6}
6687
7285
  .slm-ch-alert b{color:#fff}
6688
7286
  .slm-ch-alert button{display:block;margin-top:6px;color:#fff;font-weight:800;min-height:36px}
@@ -6779,7 +7377,8 @@ var CHANNELS_CSS = `
6779
7377
  .slm-ch-staged.shake{outline:2px solid #e5484d;outline-offset:2px}
6780
7378
  .slm-ch-staged.done{outline:2px solid #5bd39b;outline-offset:2px}
6781
7379
  }
6782
- `;
7380
+ `
7381
+ );
6783
7382
  function bucketRowsHtml(rows) {
6784
7383
  return rows.map((row, index) => `
6785
7384
  <div class="slm-ch-bucket" style="animation-delay:${Math.min(index, 4) * 30}ms">
@@ -6829,6 +7428,10 @@ var ChannelsMode = class {
6829
7428
  this.loadError = null;
6830
7429
  this.loading = true;
6831
7430
  this.view = "inspect";
7431
+ /** Pan is intentionally the initial desktop interaction. Assignment's
7432
+ * marquee is powerful, but must never make an organizer lose map navigation. */
7433
+ this.mapIntent = "pan";
7434
+ this.focusedSectionId = null;
6832
7435
  this.showArchived = false;
6833
7436
  this.detailChannelId = null;
6834
7437
  this.targetChannelId = "";
@@ -6845,6 +7448,15 @@ var ChannelsMode = class {
6845
7448
  this.links = [];
6846
7449
  this.linksChannelId = null;
6847
7450
  this.linksState = "idle";
7451
+ /**
7452
+ * Monotonic read generations — one for the channel list + allocation, one for
7453
+ * the open channel's links. Reads are concurrent (a 10s poll versus a
7454
+ * mutation's own reload), and the network does not promise to answer them in
7455
+ * order. Only the NEWEST read of each kind may write to state; an older
7456
+ * answer that arrives late is dropped, never painted.
7457
+ */
7458
+ this.listSeq = 0;
7459
+ this.linksSeq = 0;
6848
7460
  this.previewAudience = [];
6849
7461
  this.previewIncludePublic = false;
6850
7462
  this.previewProjection = null;
@@ -6871,10 +7483,14 @@ var ChannelsMode = class {
6871
7483
  enter() {
6872
7484
  if (this.active) return;
6873
7485
  this.active = true;
7486
+ this.mapIntent = "pan";
7487
+ this.focusedSectionId = null;
6874
7488
  this.ensureLayer();
6875
7489
  this.host.root.classList.add("ch-mode");
6876
7490
  this.applySheetClasses();
7491
+ if (this.host.sections().length > 1) this.host.showSectionOverview();
6877
7492
  this.paintRail();
7493
+ this.onInteractionChange?.();
6878
7494
  void this.refresh();
6879
7495
  this.pollTimer = setInterval(() => {
6880
7496
  void this.refresh({ quiet: true });
@@ -6926,6 +7542,17 @@ var ChannelsMode = class {
6926
7542
  canSelect() {
6927
7543
  return this.caps.manage && this.view === "inspect";
6928
7544
  }
7545
+ /** Bulk seat assignment is explicit. In Pan map, clicks can still inspect a
7546
+ * single seat, while a primary-button drag always moves the camera. */
7547
+ usesMarqueeSelection() {
7548
+ return this.canSelect() && this.mapIntent === "assign";
7549
+ }
7550
+ /** The renderer calls this when the organizer opens a section from overview. */
7551
+ handleSectionFocus(sectionId) {
7552
+ if (!this.active) return;
7553
+ this.focusedSectionId = sectionId;
7554
+ this.paintRail();
7555
+ }
6929
7556
  /**
6930
7557
  * Organizer realtime integration point. M5 ships a per-scope socket for
6931
7558
  * buyers; the organizer channel-count stream is a later milestone. When it
@@ -6953,22 +7580,28 @@ var ChannelsMode = class {
6953
7580
  // ---- data -----------------------------------------------------------------
6954
7581
  async refresh(opts = {}) {
6955
7582
  if (!this.caps.view) return;
7583
+ const seq = ++this.listSeq;
7584
+ const superseded = () => seq !== this.listSeq;
6956
7585
  try {
6957
7586
  const list = await this.host.api.channels(this.host.eventKey, { includeArchived: this.showArchived });
7587
+ if (superseded()) return;
6958
7588
  this.list = list;
6959
7589
  this.assignmentVersion = list.assignmentVersion;
6960
7590
  this.loadError = null;
6961
7591
  if (!this.targetChannelId) {
6962
7592
  this.targetChannelId = list.channels.find((c) => c.state === "active")?.id ?? PUBLIC_CHANNEL_ID;
6963
7593
  }
6964
- await this.loadAllocation();
7594
+ await this.loadAllocation(seq);
7595
+ if (superseded()) return;
6965
7596
  if (this.detailChannelId) await this.loadLinks(this.detailChannelId);
7597
+ if (superseded()) return;
6966
7598
  this.loading = false;
6967
7599
  if (this.active) {
6968
7600
  this.paintRail();
6969
7601
  this.paintOverlay();
6970
7602
  }
6971
7603
  } catch (err) {
7604
+ if (superseded()) return;
6972
7605
  this.loading = false;
6973
7606
  if (err instanceof ManageApiError && err.status === 403) {
6974
7607
  this.caps = { view: false, manage: false };
@@ -6980,10 +7613,11 @@ var ChannelsMode = class {
6980
7613
  }
6981
7614
  /** Walk every allocation page. Bounded by the event's seat count, and the
6982
7615
  * server caps each page, so an arena is a handful of round trips. */
6983
- async loadAllocation() {
7616
+ async loadAllocation(seq) {
6984
7617
  const next = /* @__PURE__ */ new Map();
6985
7618
  let afterLabel;
6986
7619
  for (let page = 0; page < 200; page += 1) {
7620
+ if (seq !== void 0 && seq !== this.listSeq) return;
6987
7621
  const res = await this.host.api.channelAllocation(this.host.eventKey, {
6988
7622
  afterLabel,
6989
7623
  limit: 1e3
@@ -6995,6 +7629,7 @@ var ChannelsMode = class {
6995
7629
  if (!res.nextAfterLabel) break;
6996
7630
  afterLabel = res.nextAfterLabel;
6997
7631
  }
7632
+ if (seq !== void 0 && seq !== this.listSeq) return;
6998
7633
  this.allocation = next;
6999
7634
  }
7000
7635
  // ---- lookups --------------------------------------------------------------
@@ -7061,9 +7696,10 @@ var ChannelsMode = class {
7061
7696
  * Repaint the allocation (or preview) overlay in ONE canvas pass.
7062
7697
  *
7063
7698
  * Channel identity on the map is a fill in the administrative color PLUS the
7064
- * letter flags below — never color alone. Physical status keeps its own cue:
7065
- * only FREE units take a channel fill, so sold/held/blocked seats still read
7066
- * exactly as they do in every other tool.
7699
+ * letter flags below — never color alone. In buyer preview the map instead
7700
+ * uses two explicit, channel-neutral access states. Physical status keeps its
7701
+ * own cue: only FREE units are repainted, so sold/held/blocked seats still
7702
+ * read exactly as they do in every other tool.
7067
7703
  */
7068
7704
  paintOverlay() {
7069
7705
  const canvas = this.canvas;
@@ -7088,11 +7724,14 @@ var ChannelsMode = class {
7088
7724
  if (!ctx) return;
7089
7725
  ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
7090
7726
  ctx.clearRect(0, 0, width, height);
7727
+ const seatDetail = this.host.isSeatDetail();
7091
7728
  const size = Math.max(3, this.host.seatPixelSize());
7092
7729
  const half = size / 2;
7093
7730
  const projection = this.view === "preview" ? this.previewProjection : null;
7094
7731
  const eligible = projection ? new Set(projection.available === false ? [] : projection.eligible ?? []) : null;
7095
7732
  const clusters = /* @__PURE__ */ new Map();
7733
+ const sectionTargets = !seatDetail && this.host.sections().length > 1 ? /* @__PURE__ */ new Map() : null;
7734
+ const previewSections = this.view === "preview" && seatDetail && size <= 15 ? /* @__PURE__ */ new Map() : null;
7096
7735
  for (const seat of this.host.seats()) {
7097
7736
  const status = this.host.statusOf(seat.label) ?? "free";
7098
7737
  const channelId = this.allocation.get(seat.label) ?? PUBLIC_CHANNEL_ID;
@@ -7103,29 +7742,155 @@ var ChannelsMode = class {
7103
7742
  cluster.n += 1;
7104
7743
  clusters.set(channelId, cluster);
7105
7744
  }
7745
+ if (!seatDetail) {
7746
+ const section = this.host.sectionOfLabel(seat.label);
7747
+ const point2 = section ? this.host.worldToScreen({ x: seat.x, y: seat.y }) : null;
7748
+ if (sectionTargets && section && point2) {
7749
+ const bounds = sectionTargets.get(section.id) ?? {
7750
+ label: section.label,
7751
+ minX: point2.x,
7752
+ minY: point2.y,
7753
+ maxX: point2.x,
7754
+ maxY: point2.y
7755
+ };
7756
+ bounds.minX = Math.min(bounds.minX, point2.x);
7757
+ bounds.minY = Math.min(bounds.minY, point2.y);
7758
+ bounds.maxX = Math.max(bounds.maxX, point2.x);
7759
+ bounds.maxY = Math.max(bounds.maxY, point2.y);
7760
+ sectionTargets.set(section.id, bounds);
7761
+ }
7762
+ continue;
7763
+ }
7106
7764
  if (status !== "free") continue;
7107
7765
  let fill = null;
7766
+ let stroke = null;
7108
7767
  if (this.view === "preview") {
7109
- fill = eligible ? eligible.has(seat.label) ? null : "#3a4051" : null;
7768
+ if (eligible?.has(seat.label)) {
7769
+ fill = PREVIEW_ELIGIBLE_FILL;
7770
+ stroke = PREVIEW_ELIGIBLE_STROKE;
7771
+ } else {
7772
+ fill = PREVIEW_UNAVAILABLE_FILL;
7773
+ stroke = PREVIEW_UNAVAILABLE_STROKE;
7774
+ }
7110
7775
  } else if (channelId !== PUBLIC_CHANNEL_ID) {
7111
7776
  fill = this.markerFor(channelId).color;
7777
+ stroke = ALLOCATION_STROKE;
7112
7778
  }
7113
7779
  if (!fill) continue;
7114
7780
  const point = this.host.worldToScreen({ x: seat.x, y: seat.y });
7115
7781
  if (!point) continue;
7116
7782
  if (point.x < -size || point.y < -size || point.x > width + size || point.y > height + size) continue;
7783
+ if (previewSections) {
7784
+ const section = this.host.sectionOfLabel(seat.label);
7785
+ if (section) {
7786
+ const bounds = previewSections.get(section.id) ?? {
7787
+ label: section.label,
7788
+ minX: point.x,
7789
+ minY: point.y,
7790
+ maxX: point.x,
7791
+ maxY: point.y
7792
+ };
7793
+ bounds.minX = Math.min(bounds.minX, point.x);
7794
+ bounds.minY = Math.min(bounds.minY, point.y);
7795
+ bounds.maxX = Math.max(bounds.maxX, point.x);
7796
+ bounds.maxY = Math.max(bounds.maxY, point.y);
7797
+ previewSections.set(section.id, bounds);
7798
+ }
7799
+ }
7117
7800
  ctx.fillStyle = fill;
7118
- ctx.globalAlpha = this.view === "preview" ? 0.9 : 0.85;
7119
- ctx.fillRect(point.x - half, point.y - half, size, size);
7801
+ if (this.view === "preview" || channelId !== PUBLIC_CHANNEL_ID) {
7802
+ const radius = Math.max(2, half + Math.min(1.5, half * 0.06));
7803
+ ctx.globalAlpha = 1;
7804
+ ctx.beginPath();
7805
+ ctx.arc(point.x, point.y, radius, 0, Math.PI * 2);
7806
+ ctx.fill();
7807
+ ctx.strokeStyle = stroke ?? fill;
7808
+ ctx.lineWidth = this.view === "preview" ? Math.max(1, Math.min(1.75, size * 0.13)) : Math.max(1, Math.min(1.5, size * 0.1));
7809
+ ctx.stroke();
7810
+ if (this.view === "preview" && eligible?.has(seat.label) && size >= 22) {
7811
+ this.paintPreviewSeatLabel(ctx, seat.label, point.x, point.y, radius);
7812
+ }
7813
+ } else {
7814
+ ctx.globalAlpha = 0.85;
7815
+ ctx.fillRect(point.x - half, point.y - half, size, size);
7816
+ }
7120
7817
  }
7121
7818
  ctx.globalAlpha = 1;
7819
+ if (previewSections) this.paintPreviewSectionLabels(ctx, previewSections);
7820
+ this.paintSectionTargets(sectionTargets);
7122
7821
  this.paintFlags(clusters);
7123
7822
  }
7823
+ /** Draw an eligible seat's actual chart label without inventing a new buyer
7824
+ * identifier. Long labels scale down and are omitted rather than overflowing
7825
+ * into an adjacent seat. */
7826
+ paintPreviewSeatLabel(ctx, label, x, y, radius) {
7827
+ const maxWidth = radius * 1.55;
7828
+ let fontSize = Math.min(13, Math.max(7, radius * 0.55));
7829
+ const minFontSize = 6;
7830
+ while (fontSize >= minFontSize) {
7831
+ ctx.font = `800 ${fontSize}px var(--slm-font, system-ui, sans-serif)`;
7832
+ if (ctx.measureText(label).width <= maxWidth) break;
7833
+ fontSize -= 0.5;
7834
+ }
7835
+ if (fontSize < minFontSize) return;
7836
+ ctx.fillStyle = "#ffffff";
7837
+ ctx.textAlign = "center";
7838
+ ctx.textBaseline = "middle";
7839
+ ctx.fillText(label, x, y);
7840
+ }
7841
+ /** A section overview is a navigation map. These transparent, keyboardable
7842
+ * hit areas sit over the renderer's section shells so both mouse and keyboard
7843
+ * always take the organizer into the real focused-section camera state. */
7844
+ paintSectionTargets(sections) {
7845
+ const layer = this.layer;
7846
+ if (!layer) return;
7847
+ layer.querySelectorAll(".slm-ch-section-target").forEach((el2) => el2.remove());
7848
+ if (!sections) return;
7849
+ for (const [id, section] of sections) {
7850
+ const width = section.maxX - section.minX;
7851
+ const height = section.maxY - section.minY;
7852
+ if (width < 20 || height < 20) continue;
7853
+ const target = document.createElement("button");
7854
+ target.type = "button";
7855
+ target.className = "slm-ch-section-target";
7856
+ target.style.left = `${section.minX - 8}px`;
7857
+ target.style.top = `${section.minY - 8}px`;
7858
+ target.style.width = `${width + 16}px`;
7859
+ target.style.height = `${height + 16}px`;
7860
+ target.setAttribute("aria-label", `Open ${section.label} seats`);
7861
+ target.addEventListener("click", () => {
7862
+ this.focusedSectionId = id;
7863
+ this.host.focusSection(id);
7864
+ this.paintRail();
7865
+ });
7866
+ layer.appendChild(target);
7867
+ }
7868
+ }
7869
+ /** Keep renderer section names legible over a dense, zoomed-out preview. */
7870
+ paintPreviewSectionLabels(ctx, sections) {
7871
+ for (const section of sections.values()) {
7872
+ const width = section.maxX - section.minX;
7873
+ const height = section.maxY - section.minY;
7874
+ if (width < 52 || height < 26) continue;
7875
+ const centerX = (section.minX + section.maxX) / 2;
7876
+ const centerY = (section.minY + section.maxY) / 2;
7877
+ const fontSize = Math.max(11, Math.min(15, height * 0.16));
7878
+ ctx.font = `800 ${fontSize}px var(--slm-font, system-ui, sans-serif)`;
7879
+ const labelWidth = Math.min(width - 8, ctx.measureText(section.label).width + 18);
7880
+ const labelHeight = fontSize + 10;
7881
+ ctx.fillStyle = "rgba(11, 16, 28, .88)";
7882
+ ctx.fillRect(centerX - labelWidth / 2, centerY - labelHeight / 2, labelWidth, labelHeight);
7883
+ ctx.fillStyle = "#f8fafc";
7884
+ ctx.textAlign = "center";
7885
+ ctx.textBaseline = "middle";
7886
+ ctx.fillText(section.label, centerX, centerY);
7887
+ }
7888
+ }
7124
7889
  /** Letter flags at each channel's centroid — the non-color identity cue. */
7125
7890
  paintFlags(clusters) {
7126
7891
  const layer = this.layer;
7127
7892
  if (!layer) return;
7128
- layer.querySelectorAll(".slm-ch-flag").forEach((el) => el.remove());
7893
+ layer.querySelectorAll(".slm-ch-flag").forEach((el2) => el2.remove());
7129
7894
  if (this.view === "preview") return;
7130
7895
  const ranked = [...clusters.entries()].sort((a, b) => b[1].n - a[1].n).slice(0, MAX_FLAGS);
7131
7896
  for (const [channelId, cluster] of ranked) {
@@ -7186,7 +7951,7 @@ var ChannelsMode = class {
7186
7951
  });
7187
7952
  });
7188
7953
  }
7189
- setBanner(on, name = "") {
7954
+ setBanner(on, name = "", eligibleSeats) {
7190
7955
  const banner = this.bannerEl;
7191
7956
  if (!banner) return;
7192
7957
  this.host.root.classList.toggle("ch-preview", on);
@@ -7196,8 +7961,9 @@ var ChannelsMode = class {
7196
7961
  return;
7197
7962
  }
7198
7963
  const marker = this.previewAudience.length === 1 ? this.markerFor(this.previewAudience[0]) : { color: "var(--slm-accent)", letter: "" };
7964
+ const availability = eligibleSeats == null ? "" : ` \xB7 ${eligibleSeats.toLocaleString()} ${eligibleSeats === 1 ? "seat" : "seats"} available now`;
7199
7965
  banner.innerHTML = `<span class="dot" style="background:${esc(marker.color)}"></span>
7200
- Previewing buyer access \xB7 ${esc(name)} \xB7 read-only
7966
+ Previewing buyer access \xB7 ${esc(name)}${availability} \xB7 read-only
7201
7967
  <button type="button" data-ch-act="exit-preview">Exit preview</button>`;
7202
7968
  banner.classList.add("on");
7203
7969
  banner.querySelector('[data-ch-act="exit-preview"]')?.addEventListener("click", () => this.setView("inspect"));
@@ -7232,6 +7998,8 @@ var ChannelsMode = class {
7232
7998
  includePublic: this.previewIncludePublic || audience.some(isPublicChannelId)
7233
7999
  });
7234
8000
  this.previewSupported = true;
8001
+ const eligibleSeats = this.previewProjection.available === false ? void 0 : this.previewProjection.counts?.eligible ?? this.previewProjection.eligible?.length;
8002
+ this.setBanner(true, names, eligibleSeats);
7235
8003
  } catch (err) {
7236
8004
  const status = err instanceof ManageApiError ? err.status : 0;
7237
8005
  this.previewSupported = !(status === 404 || status === 405 || status === 501);
@@ -7282,6 +8050,22 @@ var ChannelsMode = class {
7282
8050
  aria-pressed="${this.view === "inspect"}">Inspect allocation</button>
7283
8051
  <button type="button" class="${previewOn.trim()}" data-ch-view="preview"
7284
8052
  aria-pressed="${this.view === "preview"}">Preview buyer access</button>
8053
+ </div>${this.mapNavigationHtml()}`;
8054
+ }
8055
+ mapNavigationHtml() {
8056
+ if (this.host.sections().length < 2) return "";
8057
+ const focused = this.focusedSectionId ? this.host.sections().find((section) => section.id === this.focusedSectionId)?.label ?? "section" : null;
8058
+ const panOn = this.mapIntent === "pan" ? " on" : "";
8059
+ const assignOn = this.mapIntent === "assign" ? " on" : "";
8060
+ const intent = this.view === "inspect" && this.caps.manage ? `<div class="slm-ch-viewseg" role="group" aria-label="Map interaction">
8061
+ <button type="button" class="${panOn.trim()}" data-ch-map="pan" aria-pressed="${this.mapIntent === "pan"}">Pan map</button>
8062
+ <button type="button" class="${assignOn.trim()}" data-ch-map="assign" aria-pressed="${this.mapIntent === "assign"}">Assign seats</button>
8063
+ </div>
8064
+ <p>${this.mapIntent === "pan" ? "Drag to explore. Click a section to open its seats." : "Drag across seats to select them for allocation."}</p>` : "<p>Drag to explore. Click a section to open its seats.</p>";
8065
+ return `<div class="slm-ch-mapnav">
8066
+ <div class="slm-ch-mapnav-head"><span>${focused ? `Viewing ${esc(focused)}` : "Section overview"}</span>
8067
+ <button type="button" data-ch-act="sections">All sections</button></div>
8068
+ ${intent}
7285
8069
  </div>`;
7286
8070
  }
7287
8071
  countsHtml(counts, key) {
@@ -7420,6 +8204,8 @@ var ChannelsMode = class {
7420
8204
  */
7421
8205
  async loadLinks(channelId) {
7422
8206
  if (!this.caps.view) return;
8207
+ const seq = ++this.linksSeq;
8208
+ const superseded = () => seq !== this.linksSeq || this.linksChannelId !== channelId;
7423
8209
  if (this.linksChannelId !== channelId) {
7424
8210
  this.links = [];
7425
8211
  this.linksChannelId = channelId;
@@ -7427,11 +8213,11 @@ var ChannelsMode = class {
7427
8213
  }
7428
8214
  try {
7429
8215
  const res = await this.host.api.accessLinks(this.host.eventKey, channelId);
7430
- if (this.linksChannelId !== channelId) return;
8216
+ if (superseded()) return;
7431
8217
  this.links = res.links ?? [];
7432
8218
  this.linksState = "ready";
7433
8219
  } catch (err) {
7434
- if (this.linksChannelId !== channelId) return;
8220
+ if (superseded()) return;
7435
8221
  const status = err instanceof ManageApiError ? err.status : 0;
7436
8222
  this.links = [];
7437
8223
  this.linksState = status === 404 || status === 405 || status === 501 ? "unsupported" : "error";
@@ -7508,9 +8294,9 @@ var ChannelsMode = class {
7508
8294
  const unavailable = this.previewProjection?.available === false ? `<div class="slm-ch-alert warn" role="status"><span>\u23F8</span>
7509
8295
  <span><b>This private sale is not available.</b> ${esc((this.previewProjection.unavailable ?? []).map((entry) => `${this.nameOf(entry.channelId) ?? "This channel"} is ${entry.state}`).join("; ") || "The audience cannot buy right now")}.
7510
8296
  A buyer arriving with this access sees this message, not these seats.</span></div>` : "";
7511
- const counts = this.previewProjection?.counts;
7512
- const summary = counts?.eligible != null && this.previewProjection?.available !== false ? `<div class="slm-ch-alert warn"><span>\u2139</span><span>${counts.eligible.toLocaleString()} seats are buyable
7513
- through this access.${this.previewProjection?.includePublic === false ? " Public sale seats are <b>not</b> included in this grant." : ""}</span></div>` : "";
8297
+ const eligibleSeats = this.previewProjection?.counts?.eligible ?? this.previewProjection?.eligible?.length;
8298
+ const summary = eligibleSeats != null && this.previewProjection?.available !== false ? `<div class="slm-ch-alert info"><span>\u2713</span><span><b>${eligibleSeats.toLocaleString()} ${eligibleSeats === 1 ? "seat is" : "seats are"} available now.</b>
8299
+ This is the exact buyer-visible allocation.${this.previewProjection?.includePublic === false ? " Public sale seats are <b>not</b> included in this access." : ""}</span></div>` : "";
7514
8300
  const includePublic = isPublicChannelId(current) ? "" : `
7515
8301
  <label class="slm-note" style="display:flex;gap:8px;align-items:center;margin:10px 0">
7516
8302
  <input type="checkbox" data-ch-includepublic ${this.previewIncludePublic ? "checked" : ""} />
@@ -7541,6 +8327,13 @@ var ChannelsMode = class {
7541
8327
  rail.querySelectorAll("[data-ch-view]").forEach((button) => {
7542
8328
  button.addEventListener("click", () => this.setView(button.dataset.chView));
7543
8329
  });
8330
+ rail.querySelectorAll("[data-ch-map]").forEach((button) => {
8331
+ button.addEventListener("click", () => {
8332
+ this.mapIntent = button.dataset.chMap === "assign" ? "assign" : "pan";
8333
+ this.paintRail();
8334
+ this.onInteractionChange?.();
8335
+ });
8336
+ });
7544
8337
  rail.querySelectorAll("[data-ch-detail]").forEach((button) => {
7545
8338
  button.addEventListener("click", () => {
7546
8339
  const channelId = button.dataset.chDetail;
@@ -7591,6 +8384,11 @@ var ChannelsMode = class {
7591
8384
  }
7592
8385
  railAction(action) {
7593
8386
  switch (action) {
8387
+ case "sections":
8388
+ this.focusedSectionId = null;
8389
+ this.host.showSectionOverview();
8390
+ this.paintRail();
8391
+ break;
7594
8392
  case "create":
7595
8393
  this.openDialog({ kind: "create" });
7596
8394
  break;
@@ -7765,7 +8563,7 @@ var ChannelsMode = class {
7765
8563
  <label>Marker</label>
7766
8564
  <div style="display:flex;gap:8px;align-items:center">
7767
8565
  <span class="slm-ch-mk" data-ch-marker style="background:${esc(suggestion.color)};width:28px;height:28px;font-size:13px">${esc(suggestion.letter)}</span>
7768
- <span class="slm-note" style="margin:0">Letter + colour suggested from the name. Buyers never see either.</span>
8566
+ <span class="slm-note" style="margin:0">Letter comes from the name; colour is chosen automatically from the next available palette. Buyers never see either.</span>
7769
8567
  </div>
7770
8568
  </div>
7771
8569
  <div class="slm-field">
@@ -8116,6 +8914,24 @@ var ChannelsMode = class {
8116
8914
  await this.loadLinks(channelId);
8117
8915
  this.paintRail();
8118
8916
  }
8917
+ /**
8918
+ * The reload EVERY link mutation owes the panel.
8919
+ *
8920
+ * A create/rotate/revoke changes two things the detail panel renders: the
8921
+ * channel's access line (the server sets `access.intent` on create, and clears
8922
+ * it when the last live link goes) and the link status list. Both are re-read
8923
+ * here and the rail repainted, so the panel the organizer is already looking
8924
+ * at is current the moment the mutation lands — no reload, and no dependence
8925
+ * on HOW the one-time reveal was dismissed (the button, Escape, or never).
8926
+ */
8927
+ async reloadAfterLinkChange(channelId) {
8928
+ if (this.detailChannelId === channelId) {
8929
+ await this.refresh({ quiet: true });
8930
+ return;
8931
+ }
8932
+ await this.loadLinks(channelId);
8933
+ if (this.active) this.paintRail();
8934
+ }
8119
8935
  linkById(linkId) {
8120
8936
  return this.links.find((link) => link.id === linkId) ?? null;
8121
8937
  }
@@ -8209,7 +9025,7 @@ var ChannelsMode = class {
8209
9025
  try {
8210
9026
  const reveal = await this.host.api.createAccessLink(this.host.eventKey, channelId, input);
8211
9027
  this.revealLink(reveal, { channelId });
8212
- await this.refresh({ quiet: true });
9028
+ await this.reloadAfterLinkChange(channelId);
8213
9029
  } catch (err) {
8214
9030
  this.showDialogError(accessLinkErrorCopy(err instanceof ManageApiError ? err : void 0));
8215
9031
  if (!(err instanceof ManageApiError)) this.host.onError(err);
@@ -8267,9 +9083,6 @@ var ChannelsMode = class {
8267
9083
  }
8268
9084
  selectSecret(dialog);
8269
9085
  });
8270
- dialog.querySelector("[data-ch-lk-done]")?.addEventListener("click", () => {
8271
- void this.loadLinks(opts.channelId).then(() => this.paintRail());
8272
- });
8273
9086
  });
8274
9087
  this.announce("Your hosted access link is ready and is shown once.");
8275
9088
  }
@@ -8335,6 +9148,7 @@ var ChannelsMode = class {
8335
9148
  endActiveSessions
8336
9149
  );
8337
9150
  this.revealLink(reveal, { channelId, rotated: true });
9151
+ await this.reloadAfterLinkChange(channelId);
8338
9152
  } catch (err) {
8339
9153
  this.showDialogError(accessLinkErrorCopy(err instanceof ManageApiError ? err : void 0));
8340
9154
  if (!(err instanceof ManageApiError)) this.host.onError(err);
@@ -8377,9 +9191,7 @@ var ChannelsMode = class {
8377
9191
  endActiveSessions
8378
9192
  );
8379
9193
  this.closeDialog();
8380
- await this.loadLinks(channelId);
8381
- await this.refresh({ quiet: true });
8382
- this.paintRail();
9194
+ await this.reloadAfterLinkChange(channelId);
8383
9195
  this.host.toast(res.endedSessions ? `Link revoked. ${res.endedSessions.toLocaleString()} buyer${res.endedSessions === 1 ? "" : "s"} lost access.` : "Link revoked. It no longer opens for anyone.", "ok");
8384
9196
  } catch (err) {
8385
9197
  this.showDialogError(accessLinkErrorCopy(err instanceof ManageApiError ? err : void 0));
@@ -8441,9 +9253,9 @@ function toLocalInput(ms) {
8441
9253
  }
8442
9254
  function resolveContainer4(container) {
8443
9255
  if (typeof container === "string") {
8444
- const el = document.querySelector(container);
8445
- if (!el) throw new Error(`seatmanager: container "${container}" not found`);
8446
- return el;
9256
+ const el2 = document.querySelector(container);
9257
+ if (!el2) throw new Error(`seatmanager: container "${container}" not found`);
9258
+ return el2;
8447
9259
  }
8448
9260
  if (!(container instanceof HTMLElement)) {
8449
9261
  throw new Error("seatmanager: container must be a CSS selector or an HTMLElement");
@@ -8454,7 +9266,7 @@ function toRenderStatus(s) {
8454
9266
  return s === "blocked" ? "not_for_sale" : s;
8455
9267
  }
8456
9268
  var DEFAULT_API_BASE3 = "https://api.seatlayer.io";
8457
- var STYLE_ID2 = "seatlayer-manager-style";
9269
+ var STYLE_ID3 = "seatlayer-manager-style";
8458
9270
  var FEED_CAP = 80;
8459
9271
  var MAX_LIVE_SEAT_PULSES = 16;
8460
9272
  var MAX_LIVE_SECTION_PULSES = 4;
@@ -8464,9 +9276,19 @@ var LEGEND = [
8464
9276
  { key: "booked", label: "Booked", color: "#22a06b" },
8465
9277
  { key: "blocked", label: "Blocked", color: "#8b94ac" }
8466
9278
  ];
8467
- var CSS2 = `
9279
+ var MANAGER_CSS = (
9280
+ /* @sl-css */
9281
+ `
8468
9282
  .slm{position:relative;display:flex;flex-direction:column;width:100%;height:100%;min-height:480px;overflow:hidden;
8469
- background:var(--slm-bg);color:var(--slm-text);font-family:var(--slm-font);border-radius:var(--slm-radius)}
9283
+ background:var(--slm-bg);color:var(--slm-text);font-family:var(--slm-font);border-radius:var(--slm-radius);
9284
+ /* Motion tokens (motion-system \xA72), declared by the cockpit ROOT rather than
9285
+ borrowed from CHANNELS_CSS. The base cockpit animates whether or not
9286
+ Channels mode is in use, so owning its own tokens is what stops a token
9287
+ edit from silently changing only half the surface. Channels mode declares
9288
+ the identical values so an embed of it stays self-contained. */
9289
+ --slm-mo-instant:80ms;--slm-mo-quick:140ms;--slm-mo-base:200ms;--slm-mo-slow:320ms;--slm-mo-ambient:2000ms;
9290
+ --slm-mo-out:cubic-bezier(.2,.8,.2,1);--slm-mo-in-out:cubic-bezier(.4,0,.2,1);--slm-mo-exit:cubic-bezier(.4,0,1,1);
9291
+ --slm-mo-spring:cubic-bezier(.34,1.3,.64,1)}
8470
9292
  .slm *{box-sizing:border-box;margin:0;padding:0}
8471
9293
  .slm button{font:inherit;color:inherit;background:none;border:0;cursor:pointer}
8472
9294
  .slm input{font:inherit}
@@ -8479,7 +9301,8 @@ var CSS2 = `
8479
9301
  .slm-mode.on{background:var(--slm-accent);color:var(--slm-accent-ink)}
8480
9302
  .slm-live{display:inline-flex;align-items:center;gap:6px;font-size:11px;letter-spacing:.12em;font-weight:800;color:var(--slm-muted)}
8481
9303
  .slm-live-dot{width:8px;height:8px;border-radius:50%;background:#8b94ac}
8482
- .slm.live .slm-live-dot{background:#22a06b;box-shadow:0 0 0 0 rgba(34,160,107,.55);animation:slm-pulse 2s infinite}
9304
+ .slm.live .slm-live-dot{background:#22a06b;box-shadow:0 0 0 0 rgba(34,160,107,.55);
9305
+ animation:slm-pulse var(--slm-mo-ambient) infinite}
8483
9306
  @keyframes slm-pulse{0%{box-shadow:0 0 0 0 rgba(34,160,107,.5)}70%{box-shadow:0 0 0 7px rgba(34,160,107,0)}100%{box-shadow:0 0 0 0 rgba(34,160,107,0)}}
8484
9307
  .slm-kpis{grid-column:1/-1;display:grid;grid-template-columns:repeat(8,minmax(0,1fr));width:100%;padding-top:10px;
8485
9308
  border-top:1px solid var(--slm-line)}
@@ -8488,7 +9311,11 @@ var CSS2 = `
8488
9311
  font-variant-numeric:tabular-nums;white-space:nowrap}
8489
9312
  .slm-kpi span{font-size:9.5px;letter-spacing:.1em;text-transform:uppercase;color:var(--slm-muted);font-weight:700}
8490
9313
  .slm-kpi .dot{display:inline-block;width:8px;height:8px;border-radius:50%;margin-right:5px;vertical-align:baseline}
8491
- .slm-kpi.changed b{animation:slm-kpi-bump .58s cubic-bezier(.2,.8,.2,1)}
9314
+ /* The one playful moment this surface is allowed (\xA72 --mo-spring). It ran at
9315
+ .58s against a 200ms catalog, which read as a different design language from
9316
+ the dashboard tile it mirrors; Channels mode's own count bump is the same
9317
+ pattern and must stay in step with it. */
9318
+ .slm-kpi.changed b{animation:slm-kpi-bump var(--slm-mo-base) var(--slm-mo-spring)}
8492
9319
  .slm-kpidelta{position:absolute;right:4px;top:-12px;padding:2px 5px;border-radius:999px;background:rgba(34,160,107,.17);
8493
9320
  color:#5bd39b!important;font-size:9px!important;letter-spacing:0!important;text-transform:none!important;white-space:nowrap;
8494
9321
  animation:slm-kpi-delta 1.45s ease-out both;pointer-events:none}
@@ -8507,12 +9334,14 @@ var CSS2 = `
8507
9334
  .slm-hud-chip{padding:6px 11px;border-radius:999px;font-size:12px;font-weight:700;background:var(--slm-surface);
8508
9335
  border:1px solid var(--slm-line);color:var(--slm-text)}
8509
9336
  .slm-zoomhint{position:absolute;left:50%;top:14px;transform:translateX(-50%);padding:6px 13px;border-radius:999px;
8510
- background:rgba(0,0,0,.55);color:#fff;font-size:12px;font-weight:700;pointer-events:none;opacity:0;transition:opacity .2s}
9337
+ background:rgba(0,0,0,.55);color:#fff;font-size:12px;font-weight:700;pointer-events:none;opacity:0;
9338
+ transition:opacity var(--slm-mo-base) var(--slm-mo-out)}
8511
9339
  .slm-zoomhint.on{opacity:1}
8512
9340
  .slm-liveevent{position:absolute;left:50%;top:14px;z-index:4;display:flex;align-items:center;gap:8px;max-width:min(560px,calc(100% - 32px));
8513
9341
  padding:8px 12px;border:1px solid var(--slm-line);border-radius:999px;background:color-mix(in srgb,var(--slm-surface) 92%,transparent);
8514
9342
  box-shadow:0 10px 34px rgba(0,0,0,.32);opacity:0;transform:translate(-50%,-8px);pointer-events:none;
8515
- transition:opacity .18s ease,transform .24s ease;backdrop-filter:blur(10px)}
9343
+ transition:opacity var(--slm-mo-quick) var(--slm-mo-out),transform var(--slm-mo-base) var(--slm-mo-out);
9344
+ backdrop-filter:blur(10px)}
8516
9345
  .slm-liveevent.on{opacity:1;transform:translate(-50%,0)}
8517
9346
  .slm.block-mode .slm-liveevent{top:52px}
8518
9347
  .slm-liveeventdot{width:8px;height:8px;border-radius:50%;flex:none}.slm-liveeventcopy{min-width:0;overflow:hidden;text-overflow:ellipsis;
@@ -8532,7 +9361,8 @@ var CSS2 = `
8532
9361
  /* activity feed */
8533
9362
  .slm-feed{display:flex;flex-direction:column;gap:0}
8534
9363
  .slm-feedrow{display:flex!important;width:100%;align-items:center;gap:9px;padding:8px 2px!important;border-bottom:1px solid var(--slm-line)!important;
8535
- border-radius:6px;font-size:12.5px;text-align:left!important;animation:slm-in .35s ease}
9364
+ border-radius:6px;font-size:12.5px;text-align:left!important;
9365
+ animation:slm-in var(--slm-mo-quick) var(--slm-mo-out)}
8536
9366
  .slm-feedrow:hover{background:rgba(255,255,255,.035)!important}
8537
9367
  @keyframes slm-in{from{opacity:0;transform:translateY(-4px)}to{opacity:1;transform:none}}
8538
9368
  .slm-feeddot{width:8px;height:8px;border-radius:50%;flex:none}
@@ -8599,7 +9429,8 @@ var CSS2 = `
8599
9429
 
8600
9430
  /* toast */
8601
9431
  .slm-toast{position:absolute;left:50%;bottom:16px;transform:translateX(-50%);padding:10px 16px;border-radius:10px;
8602
- font-size:13px;font-weight:700;box-shadow:0 8px 24px rgba(0,0,0,.28);opacity:0;pointer-events:none;transition:opacity .2s;
9432
+ font-size:13px;font-weight:700;box-shadow:0 8px 24px rgba(0,0,0,.28);opacity:0;pointer-events:none;
9433
+ transition:opacity var(--slm-mo-base) var(--slm-mo-out);
8603
9434
  background:var(--slm-surface);color:var(--slm-text);border:1px solid var(--slm-line);z-index:5}
8604
9435
  .slm-toast.on{opacity:1}
8605
9436
  .slm-toast.err{background:#c0392b;color:#fff;border-color:#c0392b}
@@ -8610,7 +9441,9 @@ var CSS2 = `
8610
9441
  .slm-barbtn.on{background:rgba(244,183,64,.13);border-color:#f4b740;color:#f7ca6b}
8611
9442
  .slm-sectionlist{display:flex;flex-direction:column;gap:8px;margin-top:4px}
8612
9443
  .slm-sectionlist + .slm-eyebrow{margin-top:18px}
8613
- .slm-sectionrow{width:100%;padding:10px!important;border:1px solid var(--slm-line)!important;border-radius:10px;background:var(--slm-surface)!important;text-align:left!important;transition:border-color .15s ease,transform .15s ease}
9444
+ .slm-sectionrow{width:100%;padding:10px!important;border:1px solid var(--slm-line)!important;border-radius:10px;
9445
+ background:var(--slm-surface)!important;text-align:left!important;
9446
+ transition:border-color var(--slm-mo-quick) var(--slm-mo-out),transform var(--slm-mo-quick) var(--slm-mo-out)}
8614
9447
  .slm-sectionrow:hover{border-color:var(--slm-muted)!important;transform:translateY(-1px)}
8615
9448
  .slm-sectiontop,.slm-sectionmeta{display:flex;align-items:center;justify-content:space-between;gap:10px}
8616
9449
  .slm-sectiontop{font-size:12.5px;font-weight:800}.slm-sectionmeta{margin-top:5px;color:var(--slm-muted);font-size:11px}
@@ -8629,7 +9462,8 @@ var CSS2 = `
8629
9462
  .slm-momentumcopy{margin-top:7px;color:var(--slm-muted);font-size:11px;line-height:1.45}
8630
9463
  /* sections: availability windows */
8631
9464
  .slm-availlist{display:flex;flex-direction:column;gap:8px;margin:2px 0 12px}
8632
- .slm-availrow{padding:10px;border:1px solid var(--slm-line);border-radius:10px;background:var(--slm-surface);transition:border-color .15s ease,opacity .15s ease}
9465
+ .slm-availrow{padding:10px;border:1px solid var(--slm-line);border-radius:10px;background:var(--slm-surface);
9466
+ transition:border-color var(--slm-mo-quick) var(--slm-mo-out),opacity var(--slm-mo-quick) var(--slm-mo-out)}
8633
9467
  .slm-availrow.zone{background:color-mix(in srgb,var(--slm-surface) 82%,#000)}
8634
9468
  .slm-availrow.hidden{opacity:.62}.slm-availrow.closed{opacity:.82}
8635
9469
  .slm-availhead{display:flex;align-items:center;gap:8px}
@@ -8669,17 +9503,31 @@ var CSS2 = `
8669
9503
  .slm.compact .slm-barbtn{flex:1;padding:6px 9px}.slm.compact .slm-kpis{grid-template-columns:repeat(4,minmax(0,1fr));gap:8px}
8670
9504
  .slm.compact .slm-kpi[data-kpi="buyers"],.slm.compact .slm-kpi[data-kpi="active-holds"],
8671
9505
  .slm.compact .slm-kpi[data-kpi="sold-pct"],.slm.compact .slm-kpi[data-kpi="gross-sales"]{display:none}
9506
+ /* Reduced motion, as a BLANKET over the cockpit subtree rather than a list of
9507
+ selectors. The list this replaces named four animations and two transitions,
9508
+ and had silently fallen behind the stylesheet: the zoom hint, the toast and
9509
+ the availability rows all still animated for a user who had asked the OS for
9510
+ none. An enumerated list has to be edited every time a rule is added, and
9511
+ nothing fails when it isn't \u2014 so it drifts. This cannot.
9512
+
9513
+ Motion is removed, never the information it carried: Channels mode's own
9514
+ block substitutes static outlines for its shake and success states, and it
9515
+ stays authoritative for those. No JS here waits on animationend or
9516
+ transitionend, so cutting them outright strands no state. */
8672
9517
  @media (prefers-reduced-motion:reduce){
8673
- .slm.live .slm-live-dot,.slm-feedrow,.slm-kpi.changed b,.slm-kpidelta{animation:none!important}
8674
- .slm-liveevent,.slm-sectionrow{transition:none!important}
9518
+ .slm,.slm *,.slm *::before,.slm *::after{
9519
+ animation:none!important;
9520
+ transition:none!important;
9521
+ scroll-behavior:auto!important}
8675
9522
  }
8676
- ${CHANNELS_CSS}`;
9523
+ ${CHANNELS_CSS}`
9524
+ );
8677
9525
  function injectStyle() {
8678
- if (typeof document === "undefined" || document.getElementById(STYLE_ID2)) return;
8679
- const el = document.createElement("style");
8680
- el.id = STYLE_ID2;
8681
- el.textContent = CSS2;
8682
- document.head.appendChild(el);
9526
+ if (typeof document === "undefined" || document.getElementById(STYLE_ID3)) return;
9527
+ const el2 = document.createElement("style");
9528
+ el2.id = STYLE_ID3;
9529
+ el2.textContent = MANAGER_CSS;
9530
+ document.head.appendChild(el2);
8683
9531
  }
8684
9532
  function themeVars(theme) {
8685
9533
  const t3 = theme ?? {};
@@ -8738,6 +9586,11 @@ var SeatManager = class {
8738
9586
  this.reconnectTimer = null;
8739
9587
  this.attempt = 0;
8740
9588
  this.closed = false;
9589
+ /** Mirrors the `live` root class, so the getter never has to read the DOM. */
9590
+ this.connectionStatus = "reconnecting";
9591
+ /** When the server last told us something. Stamped on accepted traffic only —
9592
+ * a socket that opens and says nothing has not refreshed anything. */
9593
+ this.lastMessageAt = null;
8741
9594
  this.ready = false;
8742
9595
  this.feed = [];
8743
9596
  this.feedTimer = null;
@@ -8947,6 +9800,12 @@ var SeatManager = class {
8947
9800
  },
8948
9801
  worldToScreen: (point) => this.renderer?.worldToScreen(point) ?? null,
8949
9802
  seatPixelSize: () => this.seatPixelSize(),
9803
+ isSeatDetail: () => this.renderer?.getRung?.() === "seats",
9804
+ showSectionOverview: () => {
9805
+ this.renderer?.clearSectionFocus();
9806
+ this.renderer?.setRung?.("sections");
9807
+ },
9808
+ focusSection: (sectionId) => this.renderer?.focusSection(sectionId),
8950
9809
  isCompact: () => !!this.root?.classList.contains("compact"),
8951
9810
  setMapInert: (inert) => {
8952
9811
  this.mapHost.toggleAttribute("inert", inert);
@@ -8956,13 +9815,15 @@ var SeatManager = class {
8956
9815
  onError: (err) => this.opts.onError?.(err)
8957
9816
  };
8958
9817
  }
8959
- /** Approximate on-screen seat size, for the channel overlay's marks. Derived
8960
- * from the live camera so the overlay tracks zoom without a renderer hook. */
9818
+ /** Actual on-screen seat diameter, for the channel overlay's marks. The
9819
+ * renderer's base seat radius is 9 chart units; retaining the camera scale
9820
+ * (rather than capping it) keeps every preview paint aligned with the real
9821
+ * chart geometry at deep zoom. */
8961
9822
  seatPixelSize() {
8962
9823
  const rect = this.renderer?.getVisibleWorldRect?.();
8963
9824
  const width = this.mapHost?.clientWidth ?? 0;
8964
9825
  if (!rect?.width || !width) return 6;
8965
- return Math.max(3, Math.min(24, width / rect.width * 14));
9826
+ return Math.max(3, width / rect.width * 18);
8966
9827
  }
8967
9828
  /** Toggle the normalized sales-velocity outline overlay without changing seat colors. */
8968
9829
  setHeatOverlay(enabled) {
@@ -9138,6 +9999,16 @@ var SeatManager = class {
9138
9999
  getControlRoomSnapshot(windowMinutes = this.trendWindowMinutes) {
9139
10000
  return this.setTrendWindow(windowMinutes);
9140
10001
  }
10002
+ /**
10003
+ * The realtime link's current state and the "as of" behind it.
10004
+ *
10005
+ * Pair with `onConnectionChange` for the edges: a host that mounts after a
10006
+ * drop, or re-reads on tab focus, needs to be able to ASK rather than wait
10007
+ * for the next transition that may never come.
10008
+ */
10009
+ getConnection() {
10010
+ return { status: this.connectionStatus, lastMessageAt: this.lastMessageAt };
10011
+ }
9141
10012
  getLog(opts = {}) {
9142
10013
  return this.api.log(this.key, opts);
9143
10014
  }
@@ -9202,6 +10073,10 @@ var SeatManager = class {
9202
10073
  onSelect: (seat) => this.handleSeatSelect(seat),
9203
10074
  onDeselect: () => this.syncSelection(),
9204
10075
  onMarquee: () => this.syncSelection(),
10076
+ onSectionTap: (sectionId) => {
10077
+ this.renderer?.focusSection(sectionId);
10078
+ this.channels?.handleSectionFocus(sectionId);
10079
+ },
9205
10080
  onViewChange: () => {
9206
10081
  this.updateZoomHint();
9207
10082
  this.channels?.handleViewChange();
@@ -9212,10 +10087,11 @@ var SeatManager = class {
9212
10087
  this.applyHeatOverlay();
9213
10088
  this.updateZoomHint();
9214
10089
  }
9215
- /** Block and Channels are both bulk-selection tools: marquee, ⌘A, category,
9216
- * section. The two differ only in WHICH statuses they may act on. */
10090
+ /** Block always uses a marquee. Channels only enables its marquee after the
10091
+ * organizer deliberately chooses Assign seats; Pan map keeps desktop drag
10092
+ * available for large charts. */
9217
10093
  isBulkSelectMode() {
9218
- return this.mode === "block" || this.mode === "channels" && this.channels?.canSelect() === true;
10094
+ return this.mode === "block" || this.mode === "channels" && this.channels?.usesMarqueeSelection() === true;
9219
10095
  }
9220
10096
  /**
9221
10097
  * Block never touches held or booked inventory, so it cannot select it.
@@ -9225,7 +10101,7 @@ var SeatManager = class {
9225
10101
  */
9226
10102
  selectableStatuses() {
9227
10103
  if (this.mode === "block") return ["free", "not_for_sale"];
9228
- if (this.mode === "inspect" || this.isBulkSelectMode()) {
10104
+ if (this.mode === "inspect" || this.isBulkSelectMode() || this.mode === "channels" && this.channels?.canSelect() === true) {
9229
10105
  return ["free", "held", "booked", "not_for_sale"];
9230
10106
  }
9231
10107
  return [];
@@ -9328,6 +10204,7 @@ var SeatManager = class {
9328
10204
  return;
9329
10205
  }
9330
10206
  if (!msg || typeof msg !== "object") return;
10207
+ this.lastMessageAt = Date.now();
9331
10208
  const m = msg;
9332
10209
  if (Array.isArray(m.hidden) || Array.isArray(m.closed)) {
9333
10210
  this.updateEffectiveAvailability(m.hidden, m.closed);
@@ -9384,6 +10261,7 @@ var SeatManager = class {
9384
10261
  const objs = await this.api.objects(this.key);
9385
10262
  this.applySnapshot(objs.seats);
9386
10263
  this.updateEffectiveAvailability(objs.hidden, objs.closed);
10264
+ this.lastMessageAt = Date.now();
9387
10265
  } catch {
9388
10266
  }
9389
10267
  }
@@ -9800,16 +10678,16 @@ var SeatManager = class {
9800
10678
  paintModeTabs() {
9801
10679
  const available = [];
9802
10680
  this.els.modes?.querySelectorAll("[data-mode]").forEach((b) => {
9803
- const el = b;
9804
- const mode = el.dataset.mode;
10681
+ const el2 = b;
10682
+ const mode = el2.dataset.mode;
9805
10683
  const permitted = mode !== "channels" || !!this.channels;
9806
- el.hidden = !permitted;
10684
+ el2.hidden = !permitted;
9807
10685
  if (!permitted) return;
9808
- available.push({ mode, label: el.textContent ?? mode });
10686
+ available.push({ mode, label: el2.textContent ?? mode });
9809
10687
  const active = mode === this.mode;
9810
- el.classList.toggle("on", active);
9811
- el.setAttribute("aria-selected", String(active));
9812
- el.tabIndex = active ? 0 : -1;
10688
+ el2.classList.toggle("on", active);
10689
+ el2.setAttribute("aria-selected", String(active));
10690
+ el2.tabIndex = active ? 0 : -1;
9813
10691
  });
9814
10692
  const tools = this.els.tools;
9815
10693
  if (tools) {
@@ -9858,6 +10736,14 @@ var SeatManager = class {
9858
10736
  this.root?.classList.toggle("live", on);
9859
10737
  if (this.els.livetext) this.els.livetext.textContent = on ? "LIVE" : "RECONNECTING";
9860
10738
  this.paintMonitorInsights();
10739
+ const next = on ? "live" : "reconnecting";
10740
+ if (next === this.connectionStatus) return;
10741
+ this.connectionStatus = next;
10742
+ try {
10743
+ this.opts.onConnectionChange?.(this.getConnection());
10744
+ } catch (err) {
10745
+ this.opts.onError?.(err);
10746
+ }
9861
10747
  }
9862
10748
  updateZoomHint() {
9863
10749
  const hint = this.els.zoomhint;
@@ -10587,13 +11473,13 @@ var SeatManager = class {
10587
11473
  this.toast(msg, "err");
10588
11474
  }
10589
11475
  toast(msg, kind) {
10590
- const el = this.els.toast;
10591
- if (!el) return;
10592
- el.textContent = msg;
10593
- el.className = `slm-toast on ${kind}`;
11476
+ const el2 = this.els.toast;
11477
+ if (!el2) return;
11478
+ el2.textContent = msg;
11479
+ el2.className = `slm-toast on ${kind}`;
10594
11480
  if (this.toastTimer) clearTimeout(this.toastTimer);
10595
11481
  this.toastTimer = setTimeout(() => {
10596
- el.className = "slm-toast";
11482
+ el2.className = "slm-toast";
10597
11483
  }, 3200);
10598
11484
  }
10599
11485
  fail(err) {