@seatlayer/js 0.9.0 → 0.10.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
@@ -416,6 +416,14 @@ var import_core2 = require("@seatlayer/core");
416
416
  var DEFAULT_API_BASE2 = "https://api.seatlayer.io";
417
417
  var DEFAULT_MAX_SELECTION2 = 10;
418
418
  var EXTEND_PROMPT_MS = 6e4;
419
+ function pointInPolygon(x, y, poly) {
420
+ let inside = false;
421
+ for (let i = 0, j = poly.length - 1; i < poly.length; j = i++) {
422
+ const xi = poly[i].x, yi = poly[i].y, xj = poly[j].x, yj = poly[j].y;
423
+ if (yi > y !== yj > y && x < (xj - xi) * (y - yi) / (yj - yi) + xi) inside = !inside;
424
+ }
425
+ return inside;
426
+ }
419
427
  function resolveContainer3(container) {
420
428
  if (typeof container === "string") {
421
429
  const el = document.querySelector(container);
@@ -459,12 +467,44 @@ var CSS = `
459
467
  .sl-map-host{position:absolute;inset:0}
460
468
  .sl-side{width:300px;flex:none;border-left:1px solid var(--sl-line);display:flex;flex-direction:column;min-height:0;overflow-y:auto}
461
469
 
462
- /* narrow (container < 640px): side panel becomes a bottom sheet */
470
+ /* narrow (container < 640px): map-first \u2014 the map claims ~80-85% of the
471
+ container and the side panel becomes a PEEKING bottom sheet (AXS/Ticketmaster
472
+ mobile pattern). data-sheet on the root: "peek" (default: grab handle + one
473
+ summary line) / "open" (\u226450%, swipe up or auto-expand on first selection).
474
+ Swipe handling lives on the sheet head ONLY \u2014 never the map host, so the
475
+ map's raw-pointer gesture pipeline is untouched. */
463
476
  .sl-picker[data-layout="narrow"] .sl-body{flex-direction:column}
464
477
  .sl-picker[data-layout="narrow"] .sl-map{min-height:0;flex:1}
465
- .sl-picker[data-layout="narrow"] .sl-side{width:100%;max-height:46%;border-left:0;border-top:1px solid var(--sl-line)}
478
+ .sl-picker[data-layout="narrow"] .sl-side{width:100%;border-left:0;border-top:1px solid var(--sl-line);
479
+ flex:none;height:50%;transition:height .22s ease}
480
+ .sl-picker[data-layout="narrow"][data-sheet="peek"] .sl-side{height:86px;overflow:hidden}
481
+ .sl-picker[data-layout="narrow"][data-sheet="peek"] .sl-side > :not(.sl-sheet-head){display:none}
466
482
  .sl-picker[data-layout="narrow"] .sl-tray{flex:none}
467
483
  .sl-picker[data-layout="narrow"] .sl-foot{position:sticky;bottom:0;background:var(--sl-bg)}
484
+ /* touch chrome: pinch-zoom exists \u2014 hide +/\u2212 on the sheet layout (keep fit) */
485
+ .sl-picker[data-layout="narrow"] .sl-zoom [data-ref="zin"],
486
+ .sl-picker[data-layout="narrow"] .sl-zoom [data-ref="zout"]{display:none}
487
+
488
+ /* bottom-sheet head: grab handle + one-line summary (narrow only) */
489
+ .sl-sheet-head{display:none;flex-direction:column;padding:6px 16px 8px;cursor:grab;touch-action:none;
490
+ user-select:none;-webkit-user-select:none;flex:none}
491
+ .sl-picker[data-layout="narrow"] .sl-sheet-head{display:flex}
492
+ .sl-sheet-grab{width:36px;height:4px;border-radius:999px;background:var(--sl-muted);opacity:.55;margin:2px auto 7px}
493
+ .sl-sheet-peek{display:flex;align-items:center;gap:7px;font-size:13px;font-weight:700;color:var(--sl-text);
494
+ white-space:nowrap;overflow:hidden;text-overflow:ellipsis;min-height:20px}
495
+ .sl-sheet-peek .sub{color:var(--sl-muted);font-weight:600}
496
+ .sl-sheet-peek .go{margin-left:auto;color:var(--sl-accent);font-weight:800;flex:none}
497
+
498
+ /* consolidated Filters row inside the sheet (a11y chips + colorblind toggle
499
+ dock here on narrow; they live on the map / zoom column on wide) */
500
+ .sl-filtersec{display:none}
501
+ .sl-filters{display:none;gap:6px;flex-wrap:wrap;align-items:center;padding:2px 16px 10px}
502
+ .sl-picker[data-layout="narrow"] .sl-filtersec.has{display:block}
503
+ .sl-picker[data-layout="narrow"] .sl-filters.has{display:flex}
504
+ .sl-cbbtn{width:32px;height:32px;border-radius:999px;background:var(--sl-surface);border:1px solid var(--sl-line);
505
+ color:var(--sl-text);display:flex;align-items:center;justify-content:center;transition:border-color .15s}
506
+ .sl-cbbtn:hover{border-color:var(--sl-muted)}
507
+ .sl-cbbtn svg{width:14px;height:14px;stroke:currentColor;stroke-width:2;fill:none;stroke-linecap:round;stroke-linejoin:round}
468
508
 
469
509
  /* price panel */
470
510
  .sl-sec{padding:14px 16px 4px;font-size:9.5px;letter-spacing:.14em;text-transform:uppercase;color:var(--sl-muted);font-weight:700}
@@ -507,19 +547,44 @@ var CSS = `
507
547
  .sl-cta:hover{filter:brightness(1.08)}
508
548
  .sl-cta:disabled{opacity:.45;cursor:not-allowed}
509
549
 
510
- /* zoom column */
511
- .sl-zoom{position:absolute;right:12px;bottom:12px;display:flex;flex-direction:column;gap:6px;z-index:5}
550
+ /* Chrome anchor regions (Feature 6) \u2014 every persistent map overlay is APPENDED
551
+ INTO one of these positioned flex containers and flows/stacks within it, so no
552
+ two pieces of chrome free-float on top of each other. Regions never overlap:
553
+ the top strip splits into left/center/right; rails + corners own their edge. */
554
+ .sl-anchor{position:absolute;z-index:5;display:flex;gap:8px;pointer-events:none}
555
+ .sl-anchor > *{pointer-events:auto}
556
+ .sl-anchor[data-region="top-left"]{top:12px;left:12px;flex-wrap:wrap;max-width:38%}
557
+ .sl-anchor[data-region="top-center"]{top:12px;left:50%;transform:translateX(-50%);flex-direction:column;
558
+ align-items:center;max-width:44%}
559
+ .sl-anchor[data-region="top-right"]{top:12px;right:12px;justify-content:flex-end;flex-wrap:wrap;max-width:38%}
560
+ .sl-anchor[data-region="left-rail"]{top:50%;left:12px;transform:translateY(-50%);flex-direction:column;max-width:42%;gap:6px}
561
+ .sl-anchor[data-region="bottom-left"]{left:12px;bottom:12px;flex-direction:column;align-items:flex-start}
562
+ .sl-anchor[data-region="bottom-center"]{left:50%;bottom:14px;transform:translateX(-50%);z-index:9;
563
+ flex-direction:column;align-items:center;gap:8px;max-width:92%}
564
+ .sl-anchor[data-region="bottom-right"]{right:12px;bottom:12px;flex-direction:column;align-items:flex-end;gap:6px}
565
+ /* narrow: tighten the top strip so left/center can't crowd each other */
566
+ .sl-picker[data-layout="narrow"] .sl-anchor[data-region="top-left"]{max-width:30%}
567
+ .sl-picker[data-layout="narrow"] .sl-anchor[data-region="top-center"]{max-width:44%}
568
+
569
+ /* TEST MODE badge \u2014 a small pill in the top-right region (shrinks on narrow) */
570
+ .sl-testbadge{padding:5px 11px;border-radius:999px;font-size:10px;font-weight:800;letter-spacing:.1em;
571
+ text-transform:uppercase;white-space:nowrap;background:var(--sl-accent);color:var(--sl-accent-ink);
572
+ box-shadow:0 2px 8px rgba(0,0,0,.25)}
573
+ .sl-picker[data-layout="narrow"] .sl-testbadge{padding:3px 8px;font-size:8.5px;letter-spacing:.06em}
574
+
575
+ /* zoom column (flows within the bottom-right region) */
576
+ .sl-zoom{display:flex;flex-direction:column;gap:6px}
512
577
  .sl-zoom button{width:36px;height:36px;border-radius:999px;background:var(--sl-surface);border:1px solid var(--sl-line);
513
578
  color:var(--sl-text);font-size:17px;font-weight:700;display:flex;align-items:center;justify-content:center;transition:border-color .15s}
514
579
  .sl-zoom button:hover{border-color:var(--sl-muted)}
515
580
  .sl-zoom svg{width:14px;height:14px;stroke:currentColor;stroke-width:2;fill:none;stroke-linecap:round;stroke-linejoin:round}
516
581
 
517
- /* toast + boot states */
518
- .sl-toast{position:absolute;left:50%;bottom:16px;transform:translateX(-50%) translateY(6px);z-index:8;max-width:88%;
582
+ /* toast + boot states (toast flows in the bottom-center region) */
583
+ .sl-toast{transform:translateY(6px);max-width:100%;
519
584
  background:var(--sl-surface);border:1px solid var(--sl-line);color:var(--sl-text);border-radius:999px;padding:9px 16px;
520
585
  font-size:12.5px;font-weight:600;opacity:0;pointer-events:none;transition:opacity .2s,transform .2s;white-space:nowrap;
521
586
  overflow:hidden;text-overflow:ellipsis}
522
- .sl-toast.on{opacity:1;transform:translateX(-50%) translateY(0)}
587
+ .sl-toast.on{opacity:1;transform:translateY(0)}
523
588
  .sl-boot{position:absolute;inset:0;z-index:6;display:flex;flex-direction:column;align-items:center;justify-content:center;
524
589
  gap:10px;background:var(--sl-bg);font-size:13px;font-weight:600;color:var(--sl-muted)}
525
590
  .sl-boot-spin{width:24px;height:24px;border-radius:50%;border:3px solid var(--sl-line);border-top-color:var(--sl-accent);
@@ -529,12 +594,12 @@ var CSS = `
529
594
  .sl-boot-retry{margin-top:4px;padding:9px 20px;border-radius:var(--sl-r-sm);background:var(--sl-accent);
530
595
  color:var(--sl-accent-ink);font-weight:700;font-size:13px}
531
596
 
532
- /* "Need more time?" extend prompt (bottom-center over the map, above the toast) */
533
- .sl-extend{position:absolute;left:50%;bottom:16px;transform:translateX(-50%) translateY(6px);z-index:9;
534
- display:none;align-items:center;gap:12px;max-width:92%;background:var(--sl-surface);border:1px solid var(--sl-line);
597
+ /* "Need more time?" extend prompt (flows in the bottom-center region, above the toast) */
598
+ .sl-extend{transform:translateY(6px);
599
+ display:none;align-items:center;gap:12px;max-width:100%;background:var(--sl-surface);border:1px solid var(--sl-line);
535
600
  color:var(--sl-text);border-radius:14px;padding:10px 12px 10px 16px;box-shadow:0 18px 50px -18px rgba(0,0,0,.6);
536
601
  opacity:0;transition:opacity .2s,transform .2s}
537
- .sl-extend.on{display:flex;opacity:1;transform:translateX(-50%) translateY(0)}
602
+ .sl-extend.on{display:flex;opacity:1;transform:translateY(0)}
538
603
  .sl-extend-txt{font-size:12.5px;font-weight:600;line-height:1.35}
539
604
  .sl-extend-txt b{font-variant-numeric:tabular-nums}
540
605
  .sl-extend-btn{flex:none;padding:8px 14px;border-radius:999px;font-weight:800;font-size:12.5px;
@@ -553,8 +618,8 @@ var CSS = `
553
618
  .sl-booked-sub{font-size:13px;color:var(--sl-muted);line-height:1.5;max-width:320px}
554
619
  .sl-booked-seats{font-weight:700;color:var(--sl-text)}
555
620
 
556
- /* a11y filter chips (over the map, top-left) */
557
- .sl-chips{position:absolute;top:12px;left:12px;z-index:5;display:flex;gap:6px;flex-wrap:wrap;max-width:70%}
621
+ /* a11y filter chips (flow within the top-left region) */
622
+ .sl-chips{display:flex;gap:6px;flex-wrap:wrap}
558
623
  .sl-chip-f{display:inline-flex;align-items:center;gap:6px;padding:7px 12px;border-radius:999px;font-size:12px;font-weight:700;
559
624
  background:var(--sl-surface);border:1px solid var(--sl-line);color:var(--sl-muted);transition:color .15s,border-color .15s}
560
625
  .sl-chip-f:hover{color:var(--sl-text)}
@@ -594,18 +659,18 @@ var CSS = `
594
659
  .sl-chip .view:hover{color:var(--sl-text)}
595
660
  .sl-chip .view svg{width:14px;height:14px;stroke:currentColor;stroke-width:2;fill:none;stroke-linecap:round;stroke-linejoin:round}
596
661
 
597
- /* arena: LOD rung pills (top-center over the map) */
598
- .sl-rungs{position:absolute;top:12px;left:50%;transform:translateX(-50%);z-index:5;display:none;
599
- background:var(--sl-surface);border:1px solid var(--sl-line);border-radius:999px;padding:3px}
662
+ /* arena: LOD rung pills (flow within the top-center region) */
663
+ .sl-rungs{display:none;background:var(--sl-surface);border:1px solid var(--sl-line);border-radius:999px;padding:3px}
600
664
  .sl-rungs.on{display:inline-flex;gap:2px}
601
665
  .sl-rungs button{padding:6px 13px;border-radius:999px;font-size:10.5px;font-weight:800;letter-spacing:.07em;
602
666
  color:var(--sl-muted);white-space:nowrap;transition:color .15s}
603
667
  .sl-rungs button:hover{color:var(--sl-text)}
604
668
  .sl-rungs button.on{background:var(--sl-accent);color:var(--sl-accent-ink)}
669
+ /* narrow: shrink the rung pills so the centered row can't reach the corner regions */
670
+ .sl-picker[data-layout="narrow"] .sl-rungs button{padding:5px 9px;font-size:9px;letter-spacing:.03em}
605
671
 
606
- /* multi-floor switcher (center-left rail over the map) */
607
- .sl-floors{position:absolute;top:50%;left:12px;transform:translateY(-50%);z-index:5;display:none;
608
- flex-direction:column;gap:6px;max-width:42%}
672
+ /* multi-floor switcher (flows within the left-rail region) */
673
+ .sl-floors{display:none;flex-direction:column;gap:6px;max-width:100%}
609
674
  .sl-floors.on{display:flex}
610
675
  .sl-floors button{padding:7px 13px;border-radius:999px;font-size:12px;font-weight:700;background:var(--sl-surface);
611
676
  border:1px solid var(--sl-line);color:var(--sl-muted);white-space:nowrap;max-width:100%;overflow:hidden;
@@ -613,11 +678,25 @@ var CSS = `
613
678
  .sl-floors button:hover{color:var(--sl-text)}
614
679
  .sl-floors button.on{background:var(--sl-accent);color:var(--sl-accent-ink);border-color:transparent}
615
680
 
616
- /* tapped-section summary card (top-center, under the rung pills) */
617
- .sl-seccard{position:absolute;top:54px;left:50%;transform:translateX(-50%);z-index:6;width:250px;
618
- max-width:calc(100% - 24px);background:var(--sl-surface);border:1px solid var(--sl-line);border-radius:12px;
681
+ /* tapped-section summary card \u2014 docks INSIDE the top-center anchor region on
682
+ wide (flows below the rung pills, never over them, never floating over the
683
+ seats at the tap point). Auto-collapses to a slim pill once seat-picking
684
+ begins (first seat select, or a pan/zoom after the focus glide); tapping the
685
+ pill re-expands; \u2715 closes in both states. On narrow it renders as a compact
686
+ strip inside the bottom sheet's peek head \u2014 never over the canvas. */
687
+ .sl-seccard{width:250px;max-width:100%;background:var(--sl-surface);border:1px solid var(--sl-line);border-radius:12px;
619
688
  padding:12px 14px;box-shadow:0 18px 50px -18px rgba(0,0,0,.6);display:none}
620
689
  .sl-seccard.on{display:block}
690
+ /* collapsed pill (wide) */
691
+ .sl-seccard.mini{width:auto;padding:5px 7px 5px 12px;border-radius:999px;cursor:pointer}
692
+ .sl-seccard.mini.on{display:inline-flex;align-items:center;gap:7px}
693
+ .sl-seccard.mini .sl-seccard-name{font-size:12px;flex:none;max-width:120px}
694
+ .sl-seccard.mini .sl-seccard-left{font-size:11px}
695
+ /* narrow: compact strip inside the sheet head (peek area) */
696
+ .sl-seccard.strip{width:100%;padding:7px 0 0;border:0;border-radius:0;box-shadow:none;background:none;cursor:default}
697
+ .sl-seccard.strip.on{display:flex;align-items:center;gap:7px;font-size:12.5px}
698
+ .sl-seccard.strip .sl-seccard-name{font-size:12.5px}
699
+ .sl-seccard.strip .sl-seccard-price{margin-left:auto}
621
700
  .sl-seccard-head{display:flex;align-items:center;gap:8px}
622
701
  .sl-seccard-dot{width:10px;height:10px;border-radius:50%;flex:none}
623
702
  .sl-seccard-name{font-weight:800;font-size:14px;flex:1;min-width:0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
@@ -659,6 +738,20 @@ var CSS = `
659
738
  font-size:11.5px;font-weight:600;background:var(--sl-surface);border:1px solid var(--sl-line);color:var(--sl-muted);
660
739
  white-space:nowrap;pointer-events:none;max-width:90%;overflow:hidden;text-overflow:ellipsis}
661
740
 
741
+ /* F3 minimap \u2014 venue overview + live viewport rect (flows in the bottom-left region) */
742
+ .sl-minimap{border:1px solid var(--sl-line);border-radius:9px;
743
+ overflow:hidden;background:var(--sl-surface);box-shadow:0 12px 34px -14px rgba(0,0,0,.55);line-height:0;cursor:pointer}
744
+ .sl-minimap canvas{display:block}
745
+ .sl-picker[data-layout="narrow"] .sl-minimap{display:none}
746
+
747
+ /* F4 price-band filter \u2014 chip row in the side panel, above the price legend it
748
+ * dims (keeps clear of the map's top-center rungs / top-left a11y chips). */
749
+ .sl-pricef{display:flex;gap:6px;flex-wrap:wrap;padding:2px 16px 10px}
750
+ .sl-pricef .sl-chip-f{padding:6px 11px;font-size:11.5px}
751
+ /* F4 legend reflection: rows + counts for out-of-band categories read muted */
752
+ .sl-price-row.sl-dim{opacity:.4}
753
+ .sl-seccard-mix-item.sl-dim{opacity:.4}
754
+
662
755
  /* modal host */
663
756
  .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}
664
757
  .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)}
@@ -694,6 +787,8 @@ var SeatPicker = class _SeatPicker {
694
787
  this.destroyed = false;
695
788
  // chrome refs
696
789
  this.els = {};
790
+ /** Feature 6 anchor regions — positioned flex containers over the map. */
791
+ this.regions = {};
697
792
  this.ro = null;
698
793
  this.holdTimer = null;
699
794
  this.toastTimer = null;
@@ -724,6 +819,25 @@ var SeatPicker = class _SeatPicker {
724
819
  this.viewEl = null;
725
820
  this.viewCleanup = null;
726
821
  this.allSeatsCache = null;
822
+ // F3 minimap
823
+ this.miniEl = null;
824
+ this.miniCanvas = null;
825
+ this.miniBase = null;
826
+ this.miniTf = null;
827
+ // F4 price-band filter — active band's category keys (null = all prices)
828
+ this.priceBandKeys = null;
829
+ this.priceFilterEl = null;
830
+ /** Last surfaced section summary (re-rendered when the price band changes). */
831
+ this.lastSection = null;
832
+ /** Section card collapsed to its slim pill (seat-picking has begun). */
833
+ this.secCardCollapsed = false;
834
+ /** When the card was (re)shown — the focus glide's own view change must not collapse it. */
835
+ this.secCardShownAt = 0;
836
+ /** Previous tray ticket count — first 0→n transition auto-expands the mobile sheet. */
837
+ this.lastTrayCount = 0;
838
+ // narrow-layout chrome that docks into the sheet's Filters row on mobile
839
+ this.a11yChipsEl = null;
840
+ this.cbEl = null;
727
841
  // modal plumbing (set by open())
728
842
  this.modalScrim = null;
729
843
  this.prevFocus = null;
@@ -742,11 +856,15 @@ var SeatPicker = class _SeatPicker {
742
856
  currency: options.currency,
743
857
  flashOnLiveChange: true,
744
858
  colorblindSafe: options.colorblindSafe,
745
- onSelectionChange: () => this.syncTray(),
859
+ onSelectionChange: () => {
860
+ this.syncTray();
861
+ if (this.controller.getSelection().length) this.collapseSectionCard();
862
+ },
746
863
  onStatusChange: () => {
747
864
  this.syncPrices();
748
865
  this.evictTakenSelections();
749
866
  this.detectBooked();
867
+ this.refreshMinimap();
750
868
  },
751
869
  onHoldExpired: () => {
752
870
  this.hold = null;
@@ -765,6 +883,8 @@ var SeatPicker = class _SeatPicker {
765
883
  onViewChange: () => {
766
884
  this.reanchorConfirm();
767
885
  this.syncRung();
886
+ this.drawMinimapRect();
887
+ this.sectionCardOnView();
768
888
  },
769
889
  // Tapped-section glide-in → surface (or clear) the section-summary card.
770
890
  onSectionFocus: (summary) => this.showSectionCard(summary),
@@ -844,7 +964,7 @@ var SeatPicker = class _SeatPicker {
844
964
  <div class="sl-body">
845
965
  <div class="sl-map">
846
966
  <div class="sl-map-host" data-ref="map"></div>
847
- <div class="sl-zoom">
967
+ <div class="sl-zoom" data-ref="zoom">
848
968
  <button type="button" aria-label="Zoom in" data-ref="zin">+</button>
849
969
  <button type="button" aria-label="Zoom out" data-ref="zout">\u2212</button>
850
970
  <button type="button" aria-label="Fit to screen" data-ref="zfit">
@@ -854,7 +974,13 @@ var SeatPicker = class _SeatPicker {
854
974
  <div class="sl-boot" data-ref="boot"><span class="sl-boot-spin"></span>Loading seat map\u2026</div>
855
975
  <div class="sl-toast" data-ref="toast" role="status" aria-live="polite"></div>
856
976
  </div>
857
- <div class="sl-side">
977
+ <div class="sl-side" data-ref="side">
978
+ <div class="sl-sheet-head" data-ref="sheetHead">
979
+ <div class="sl-sheet-grab"></div>
980
+ <div class="sl-sheet-peek" data-ref="peek"></div>
981
+ </div>
982
+ <div class="sl-sec sl-filtersec" data-ref="filtersSec">Filters</div>
983
+ <div class="sl-filters" data-ref="filters"></div>
858
984
  <div class="sl-sec" data-ref="pricesSec">Prices</div>
859
985
  <div class="sl-prices" data-ref="prices"></div>
860
986
  <div class="sl-sec">Your seats</div>
@@ -869,14 +995,55 @@ var SeatPicker = class _SeatPicker {
869
995
  this.els[el.dataset.ref] = el;
870
996
  });
871
997
  this.mapHost = this.els.map;
872
- this.ro = new ResizeObserver(() => {
998
+ const applyLayout = () => {
873
999
  const w = root.clientWidth;
874
- root.dataset.layout = w < 640 ? "narrow" : "wide";
875
- });
1000
+ if (w <= 0) return;
1001
+ const next = w < 640 ? "narrow" : "wide";
1002
+ if (root.dataset.layout === next) return;
1003
+ root.dataset.layout = next;
1004
+ if (next === "narrow" && !root.dataset.sheet) root.dataset.sheet = "peek";
1005
+ this.dockLayoutChrome();
1006
+ };
1007
+ this.ro = new ResizeObserver(applyLayout);
876
1008
  this.ro.observe(root);
1009
+ applyLayout();
1010
+ requestAnimationFrame(applyLayout);
877
1011
  this.els.zin.addEventListener("click", () => this.controller.zoomIn());
878
1012
  this.els.zout.addEventListener("click", () => this.controller.zoomOut());
879
1013
  this.els.zfit.addEventListener("click", () => this.controller.zoomToFit());
1014
+ const head = this.els.sheetHead;
1015
+ if (head) {
1016
+ const setSheet = (open) => {
1017
+ root.dataset.sheet = open ? "open" : "peek";
1018
+ };
1019
+ let startY = 0;
1020
+ let swiped = false;
1021
+ let tracking = false;
1022
+ head.addEventListener("pointerdown", (e) => {
1023
+ tracking = true;
1024
+ swiped = false;
1025
+ startY = e.clientY;
1026
+ head.setPointerCapture?.(e.pointerId);
1027
+ });
1028
+ head.addEventListener("pointermove", (e) => {
1029
+ if (!tracking || swiped) return;
1030
+ const dy = e.clientY - startY;
1031
+ if (dy < -18) {
1032
+ setSheet(true);
1033
+ swiped = true;
1034
+ } else if (dy > 18) {
1035
+ setSheet(false);
1036
+ swiped = true;
1037
+ }
1038
+ });
1039
+ head.addEventListener("pointerup", (e) => {
1040
+ if (tracking && !swiped && Math.abs(e.clientY - startY) < 6) {
1041
+ if (!e.target.closest(".sl-seccard")) setSheet(root.dataset.sheet !== "open");
1042
+ }
1043
+ tracking = false;
1044
+ head.releasePointerCapture?.(e.pointerId);
1045
+ });
1046
+ }
880
1047
  this.tipEl = document.createElement("div");
881
1048
  this.tipEl.setAttribute("role", "tooltip");
882
1049
  this.tipEl.style.cssText = "position:absolute;z-index:7;pointer-events:none;display:none;max-width:240px;background:var(--sl-surface);color:var(--sl-text);border:1px solid var(--sl-line);border-radius:10px;padding:9px 12px;font-size:12px;line-height:1.45;";
@@ -903,13 +1070,15 @@ var SeatPicker = class _SeatPicker {
903
1070
  return this;
904
1071
  }
905
1072
  this.els.boot.remove();
1073
+ this.buildRegions();
1074
+ this.regions["bottom-right"].appendChild(this.els.zoom);
1075
+ this.regions["bottom-center"].appendChild(this.els.toast);
906
1076
  if (info.mode === "test") {
907
- const ribbon = document.createElement("div");
908
- ribbon.textContent = (0, import_core2.t)("picker.testMode");
909
- ribbon.setAttribute("aria-label", (0, import_core2.t)("picker.testMode"));
910
- ribbon.style.cssText = "position:absolute;top:18px;right:-34px;z-index:6;transform:rotate(45deg);width:140px;text-align:center;padding:4px 0;background:#f4b740;color:#1a1200;font:800 10.5px/1.4 -apple-system,BlinkMacSystemFont,sans-serif;letter-spacing:.12em;box-shadow:0 2px 8px rgba(0,0,0,.25);pointer-events:none;";
911
- this.els.map.style.overflow = "hidden";
912
- this.els.map.appendChild(ribbon);
1077
+ const badge = document.createElement("div");
1078
+ badge.className = "sl-testbadge";
1079
+ badge.textContent = (0, import_core2.t)("picker.testMode");
1080
+ badge.setAttribute("aria-label", (0, import_core2.t)("picker.testMode"));
1081
+ this.regions["top-right"].appendChild(badge);
913
1082
  }
914
1083
  const chartTheme = this.controller.doc?.theme;
915
1084
  Object.entries(resolveTokens(chartTheme, this.opts.theme)).forEach(([k, v]) => root.style.setProperty(k, v));
@@ -933,7 +1102,8 @@ var SeatPicker = class _SeatPicker {
933
1102
  const GLYPH = { wheelchair: "\u267F", companion: "\u{1F9D1}\u200D\u{1F91D}\u200D\u{1F9D1}" };
934
1103
  const mk = (key, label) => `<button type="button" class="sl-chip-f${key === "all" ? " on" : ""}" data-f="${key}">${label}</button>`;
935
1104
  chips.innerHTML = mk("all", "All seats") + [...present].map((type) => mk(type, `${GLYPH[type] ? GLYPH[type] + " " : ""}${type[0].toUpperCase()}${type.slice(1).replace(/-/g, " ")}`)).join("");
936
- this.els.map.appendChild(chips);
1105
+ this.regions["top-left"].appendChild(chips);
1106
+ this.a11yChipsEl = chips;
937
1107
  chips.querySelectorAll("button").forEach((btn) => {
938
1108
  btn.addEventListener("click", () => {
939
1109
  const f = btn.dataset.f;
@@ -945,6 +1115,8 @@ var SeatPicker = class _SeatPicker {
945
1115
  }
946
1116
  const cb = document.createElement("button");
947
1117
  cb.type = "button";
1118
+ cb.className = "sl-cbbtn";
1119
+ this.cbEl = cb;
948
1120
  cb.setAttribute("aria-label", "Toggle colorblind-friendly colors");
949
1121
  cb.setAttribute("aria-pressed", String(!!this.opts.colorblindSafe));
950
1122
  cb.innerHTML = '<svg viewBox="0 0 24 24"><path d="M1 12s4-7 11-7 11 7 11 7-4 7-11 7S1 12 1 12z"/><circle cx="12" cy="12" r="3"/></svg>';
@@ -960,19 +1132,45 @@ var SeatPicker = class _SeatPicker {
960
1132
  this.srEl.setAttribute("aria-live", "polite");
961
1133
  root.appendChild(this.srEl);
962
1134
  this.buildArenaChrome();
1135
+ this.buildMinimap();
1136
+ this.buildPriceFilter();
963
1137
  this.buildExtendPrompt();
964
1138
  this.buildBookedOverlay();
1139
+ this.dockLayoutChrome();
965
1140
  this.syncPrices();
966
1141
  this.syncTray();
967
1142
  return this;
968
1143
  }
1144
+ /**
1145
+ * Move layout-dependent chrome between its wide dock (map regions / zoom
1146
+ * column) and its narrow dock (the sheet's consolidated Filters row), and
1147
+ * re-render the section card in the form the layout wants (docked card/pill
1148
+ * on wide, sheet strip on narrow). Runs on every layout flip + once post-render.
1149
+ */
1150
+ dockLayoutChrome() {
1151
+ const narrow = this.root?.dataset.layout === "narrow";
1152
+ const filters = this.els.filters;
1153
+ if (filters) {
1154
+ if (narrow) {
1155
+ if (this.a11yChipsEl) filters.appendChild(this.a11yChipsEl);
1156
+ if (this.cbEl) filters.appendChild(this.cbEl);
1157
+ } else {
1158
+ if (this.a11yChipsEl) this.regions["top-left"]?.appendChild(this.a11yChipsEl);
1159
+ if (this.cbEl) this.els.zoom?.appendChild(this.cbEl);
1160
+ }
1161
+ const has = narrow && filters.children.length > 0;
1162
+ filters.classList.toggle("has", has);
1163
+ this.els.filtersSec?.classList.toggle("has", has);
1164
+ }
1165
+ if (this.lastSection) this.renderSectionCard(this.lastSection);
1166
+ }
969
1167
  /** The "Need more time?" prompt shown in the hold's final EXTEND_PROMPT_MS. */
970
1168
  buildExtendPrompt() {
971
1169
  const el = document.createElement("div");
972
1170
  el.className = "sl-extend";
973
1171
  el.setAttribute("role", "status");
974
1172
  el.innerHTML = `<span class="sl-extend-txt" data-ref="extendTxt"></span><button type="button" class="sl-extend-btn" data-ref="extendBtn"></button>`;
975
- this.els.map.appendChild(el);
1173
+ (this.regions["bottom-center"] ?? this.els.map).appendChild(el);
976
1174
  this.extendEl = el;
977
1175
  this.els.extendTxt = el.querySelector('[data-ref="extendTxt"]');
978
1176
  this.els.extendBtn = el.querySelector('[data-ref="extendBtn"]');
@@ -990,6 +1188,258 @@ var SeatPicker = class _SeatPicker {
990
1188
  this.bookedEl = el;
991
1189
  this.els.bookedSub = el.querySelector('[data-ref="bookedSub"]');
992
1190
  }
1191
+ // ---- Feature 6: chrome anchor regions -------------------------------------
1192
+ /**
1193
+ * Create the positioned flex containers that own every persistent map overlay.
1194
+ * Appended once after controller.render(); each chrome piece is then appended
1195
+ * INTO its region and flows within it, so nothing free-floats over anything
1196
+ * else. Regions carve the map into non-overlapping zones (top strip split into
1197
+ * left/center/right, left rail, and the three used corners).
1198
+ */
1199
+ buildRegions() {
1200
+ if (!this.els.map) return;
1201
+ const REGIONS = ["top-left", "top-center", "top-right", "left-rail", "bottom-left", "bottom-center", "bottom-right"];
1202
+ for (const region of REGIONS) {
1203
+ const el = document.createElement("div");
1204
+ el.className = "sl-anchor";
1205
+ el.dataset.region = region;
1206
+ this.els.map.appendChild(el);
1207
+ this.regions[region] = el;
1208
+ }
1209
+ }
1210
+ // ---- F3 minimap -----------------------------------------------------------
1211
+ /** Read a resolved --sl-* token value (canvas needs a real color, not var()). */
1212
+ cssVar(name) {
1213
+ return this.root ? getComputedStyle(this.root).getPropertyValue(name).trim() : "";
1214
+ }
1215
+ /** Section-bearing objects on the active floor (single-floor → doc.objects). */
1216
+ activeFloorObjects() {
1217
+ const doc = this.controller.doc;
1218
+ if (!doc) return [];
1219
+ const floors = doc.floors;
1220
+ if (floors?.length) {
1221
+ const id = this.controller.getActiveFloorId();
1222
+ return (floors.find((f) => f.id === id) ?? floors[0]).objects ?? [];
1223
+ }
1224
+ return doc.objects ?? [];
1225
+ }
1226
+ /**
1227
+ * Build the overview minimap: a static venue thumbnail (section outlines, or
1228
+ * seat dots when the chart has no sections) with the live viewport rectangle
1229
+ * drawn on top. The rect tracks pan/zoom via the constructor's onViewChange.
1230
+ */
1231
+ buildMinimap() {
1232
+ const vp = this.controller.getViewport();
1233
+ if (!vp || !this.els.map) return;
1234
+ const b = vp.bounds;
1235
+ if (!(b.width > 0 && b.height > 0)) return;
1236
+ const MAXW = 158;
1237
+ const MAXH = 118;
1238
+ const PAD = 6;
1239
+ const aspect = b.width / Math.max(1, b.height);
1240
+ let w = MAXW;
1241
+ let h = Math.round(MAXW / aspect);
1242
+ if (h > MAXH) {
1243
+ h = MAXH;
1244
+ w = Math.round(MAXH * aspect);
1245
+ }
1246
+ w = Math.max(64, w);
1247
+ h = Math.max(48, h);
1248
+ const dpr = Math.min(2, window.devicePixelRatio || 1);
1249
+ const wrap = document.createElement("div");
1250
+ wrap.className = "sl-minimap";
1251
+ wrap.setAttribute("aria-hidden", "true");
1252
+ const canvas = document.createElement("canvas");
1253
+ canvas.width = Math.round(w * dpr);
1254
+ canvas.height = Math.round(h * dpr);
1255
+ canvas.style.width = `${w}px`;
1256
+ canvas.style.height = `${h}px`;
1257
+ wrap.appendChild(canvas);
1258
+ (this.regions["bottom-left"] ?? this.els.map).appendChild(wrap);
1259
+ this.miniEl = wrap;
1260
+ this.miniCanvas = canvas;
1261
+ const scale = Math.min((w - PAD * 2) / Math.max(1, b.width), (h - PAD * 2) / Math.max(1, b.height)) * dpr;
1262
+ const offX = (w * dpr - b.width * scale) / 2 - b.x * scale;
1263
+ const offY = (h * dpr - b.height * scale) / 2 - b.y * scale;
1264
+ this.miniTf = { scale, offX, offY, dpr };
1265
+ const base = document.createElement("canvas");
1266
+ base.width = canvas.width;
1267
+ base.height = canvas.height;
1268
+ this.miniBase = base;
1269
+ wrap.addEventListener("click", (e) => this.minimapJump(e));
1270
+ this.drawMinimapStatic();
1271
+ this.drawMinimapRect();
1272
+ }
1273
+ /** Repaint the static overview + rect (floor switch, live open/close). */
1274
+ refreshMinimap() {
1275
+ if (!this.miniBase) return;
1276
+ this.drawMinimapStatic();
1277
+ this.drawMinimapRect();
1278
+ }
1279
+ /** Paint the venue overview into the offscreen base canvas. */
1280
+ drawMinimapStatic() {
1281
+ const base = this.miniBase;
1282
+ const tf = this.miniTf;
1283
+ const doc = this.controller.doc;
1284
+ if (!base || !tf || !doc) return;
1285
+ const ctx = base.getContext("2d");
1286
+ if (!ctx) return;
1287
+ ctx.clearRect(0, 0, base.width, base.height);
1288
+ const fx = (x) => x * tf.scale + tf.offX;
1289
+ const fy = (y) => y * tf.scale + tf.offY;
1290
+ const line = this.cssVar("--sl-line") || "rgba(139,147,167,.5)";
1291
+ const muted = this.cssVar("--sl-muted") || "#8b93a7";
1292
+ const accent = this.cssVar("--sl-accent") || "#6e7bff";
1293
+ const zoneColor = new Map((doc.zones ?? []).map((z) => [z.id, z.color]));
1294
+ let drewSection = false;
1295
+ for (const o of this.activeFloorObjects()) {
1296
+ if (o.type !== "section" || !o.outline || o.outline.length < 3) continue;
1297
+ drewSection = true;
1298
+ const closed = this.controller.isSectionClosed(o.id);
1299
+ const fill = closed ? muted : o.color ?? (o.zone && zoneColor.get(o.zone)) ?? accent;
1300
+ ctx.beginPath();
1301
+ o.outline.forEach((p, i) => i === 0 ? ctx.moveTo(fx(p.x), fy(p.y)) : ctx.lineTo(fx(p.x), fy(p.y)));
1302
+ ctx.closePath();
1303
+ ctx.globalAlpha = closed ? 0.26 : 0.42;
1304
+ ctx.fillStyle = fill;
1305
+ ctx.fill();
1306
+ ctx.globalAlpha = 0.85;
1307
+ ctx.lineWidth = Math.max(1, tf.dpr);
1308
+ ctx.strokeStyle = line;
1309
+ ctx.stroke();
1310
+ }
1311
+ ctx.globalAlpha = 1;
1312
+ if (!drewSection) {
1313
+ const r = Math.max(1, tf.dpr);
1314
+ for (const seat of (0, import_core2.expandChart)(doc)) {
1315
+ const cat = doc.categories.find((c) => c.key === seat.categoryKey);
1316
+ ctx.fillStyle = cat?.color ?? accent;
1317
+ ctx.beginPath();
1318
+ ctx.arc(fx(seat.x), fy(seat.y), r, 0, Math.PI * 2);
1319
+ ctx.fill();
1320
+ }
1321
+ }
1322
+ }
1323
+ /** Blit the base overview, then stroke the current viewport rectangle on top. */
1324
+ drawMinimapRect() {
1325
+ const canvas = this.miniCanvas;
1326
+ const base = this.miniBase;
1327
+ const tf = this.miniTf;
1328
+ if (!canvas || !base || !tf) return;
1329
+ const ctx = canvas.getContext("2d");
1330
+ if (!ctx) return;
1331
+ ctx.clearRect(0, 0, canvas.width, canvas.height);
1332
+ ctx.drawImage(base, 0, 0);
1333
+ const vp = this.controller.getViewport();
1334
+ if (!vp) return;
1335
+ const v = vp.visible;
1336
+ const x = v.x * tf.scale + tf.offX;
1337
+ const y = v.y * tf.scale + tf.offY;
1338
+ const w = v.width * tf.scale;
1339
+ const h = v.height * tf.scale;
1340
+ const accent = this.cssVar("--sl-accent") || "#f4b740";
1341
+ ctx.save();
1342
+ ctx.globalAlpha = 0.14;
1343
+ ctx.fillStyle = accent;
1344
+ ctx.fillRect(x, y, w, h);
1345
+ ctx.globalAlpha = 1;
1346
+ ctx.lineWidth = Math.max(1.5, tf.dpr * 1.5);
1347
+ ctx.strokeStyle = accent;
1348
+ ctx.strokeRect(x, y, w, h);
1349
+ ctx.restore();
1350
+ }
1351
+ /** Minimap click → focus the section under the point (or overview on a miss). */
1352
+ minimapJump(e) {
1353
+ const canvas = this.miniCanvas;
1354
+ const tf = this.miniTf;
1355
+ if (!canvas || !tf) return;
1356
+ const r = canvas.getBoundingClientRect();
1357
+ const px = (e.clientX - r.left) * (canvas.width / r.width);
1358
+ const py = (e.clientY - r.top) * (canvas.height / r.height);
1359
+ const wx = (px - tf.offX) / tf.scale;
1360
+ const wy = (py - tf.offY) / tf.scale;
1361
+ for (const o of this.activeFloorObjects()) {
1362
+ if (o.type !== "section" || !o.outline || o.outline.length < 3) continue;
1363
+ if (this.controller.isSectionClosed(o.id)) continue;
1364
+ if (pointInPolygon(wx, wy, o.outline)) {
1365
+ this.controller.focusSection(o.id);
1366
+ return;
1367
+ }
1368
+ }
1369
+ this.controller.overview();
1370
+ }
1371
+ // ---- F4 price-band filter -------------------------------------------------
1372
+ /** Effective price of a category (first tier when tiered, else base price). */
1373
+ catPrice(c) {
1374
+ return c.tiers?.length ? c.tiers[0].price : c.price;
1375
+ }
1376
+ /** Derive price bands: one chip per distinct price (≤5), else quantile ranges. */
1377
+ priceBands() {
1378
+ const doc = this.controller.doc;
1379
+ if (!doc) return [];
1380
+ const priced = doc.categories.map((c) => ({ key: c.key, price: this.catPrice(c) })).filter((x) => x.price != null);
1381
+ if (!priced.length) return [];
1382
+ const distinct = [...new Set(priced.map((p) => p.price))].sort((a, b) => a - b);
1383
+ if (distinct.length <= 5) {
1384
+ return distinct.map((price) => ({
1385
+ id: `p${price}`,
1386
+ label: this.money(price),
1387
+ keys: priced.filter((p) => p.price === price).map((p) => p.key),
1388
+ min: price,
1389
+ max: price
1390
+ }));
1391
+ }
1392
+ const chunk = Math.ceil(distinct.length / 4);
1393
+ const bands = [];
1394
+ for (let i = 0; i < distinct.length; i += chunk) {
1395
+ const slice = distinct.slice(i, i + chunk);
1396
+ const lo = slice[0];
1397
+ const hi = slice[slice.length - 1];
1398
+ bands.push({
1399
+ id: `b${i}`,
1400
+ label: lo === hi ? this.money(lo) : `${this.money(lo)}\u2013${this.money(hi)}`,
1401
+ keys: priced.filter((p) => p.price >= lo && p.price <= hi).map((p) => p.key),
1402
+ min: lo,
1403
+ max: hi
1404
+ });
1405
+ }
1406
+ return bands;
1407
+ }
1408
+ /**
1409
+ * Build the price-band chip row in the side panel, directly under the "Prices"
1410
+ * header and above the legend it dims. Living in the panel (not a map overlay)
1411
+ * keeps it clear of the top-center rung pills and top-left a11y chips, and it
1412
+ * rides the bottom sheet on narrow layouts. Skipped when there's <2 bands.
1413
+ */
1414
+ buildPriceFilter() {
1415
+ if (!this.els.prices || !this.els.pricesSec) return;
1416
+ const bands = this.priceBands();
1417
+ if (bands.length < 2) return;
1418
+ const wrap = document.createElement("div");
1419
+ wrap.className = "sl-pricef";
1420
+ wrap.setAttribute("role", "group");
1421
+ wrap.setAttribute("aria-label", "Filter seats by price");
1422
+ const mk = (band, label, on) => `<button type="button" class="sl-chip-f${on ? " on" : ""}" data-band="${band}">${label}</button>`;
1423
+ wrap.innerHTML = mk("all", "All prices", true) + bands.map((bd) => mk(bd.id, bd.label, false)).join("");
1424
+ this.els.pricesSec.after(wrap);
1425
+ this.priceFilterEl = wrap;
1426
+ wrap.querySelectorAll("button").forEach((btn) => {
1427
+ btn.addEventListener("click", () => {
1428
+ wrap.querySelectorAll("button").forEach((b) => b.classList.toggle("on", b === btn));
1429
+ const id = btn.dataset.band;
1430
+ if (id === "all") {
1431
+ this.priceBandKeys = null;
1432
+ this.controller.setCategoryFilter(null);
1433
+ } else {
1434
+ const bd = bands.find((x) => x.id === id);
1435
+ this.priceBandKeys = bd ? new Set(bd.keys) : null;
1436
+ this.controller.setCategoryFilter(bd ? bd.keys : null);
1437
+ }
1438
+ this.syncPrices();
1439
+ if (this.lastSection) this.showSectionCard(this.lastSection);
1440
+ });
1441
+ });
1442
+ }
993
1443
  // ---- arena / multi-floor chrome -------------------------------------------
994
1444
  /** Build the rung pills (charts with sections) and floor switcher (>1 floor). */
995
1445
  buildArenaChrome() {
@@ -1018,7 +1468,7 @@ var SeatPicker = class _SeatPicker {
1018
1468
  pills.querySelectorAll("button").forEach((btn) => {
1019
1469
  btn.addEventListener("click", () => this.controller.setRung(btn.dataset.rung));
1020
1470
  });
1021
- this.els.map.appendChild(pills);
1471
+ this.regions["top-center"].appendChild(pills);
1022
1472
  this.rungsEl = pills;
1023
1473
  this.syncRung();
1024
1474
  }
@@ -1035,9 +1485,10 @@ var SeatPicker = class _SeatPicker {
1035
1485
  this.showSectionCard(null);
1036
1486
  this.syncFloors();
1037
1487
  this.syncRung();
1488
+ this.refreshMinimap();
1038
1489
  });
1039
1490
  });
1040
- this.els.map.appendChild(rail);
1491
+ this.regions["left-rail"].appendChild(rail);
1041
1492
  this.floorsEl = rail;
1042
1493
  this.syncFloors();
1043
1494
  }
@@ -1062,27 +1513,105 @@ var SeatPicker = class _SeatPicker {
1062
1513
  }
1063
1514
  /** Show (or clear, on null) the tapped-section summary card. */
1064
1515
  showSectionCard(summary) {
1065
- if (!summary) {
1066
- this.secCardEl?.remove();
1067
- this.secCardEl = null;
1068
- return;
1069
- }
1516
+ this.lastSection = summary;
1517
+ this.secCardEl?.remove();
1518
+ this.secCardEl = null;
1519
+ if (!summary) return;
1520
+ this.secCardCollapsed = false;
1521
+ this.secCardShownAt = Date.now();
1522
+ this.renderSectionCard(summary);
1523
+ }
1524
+ /**
1525
+ * Render the section card in the form the layout + state want: expanded card
1526
+ * or slim pill in the top-center anchor region (wide), or a compact strip in
1527
+ * the sheet head (narrow). Never floats over the seats at the tap point.
1528
+ */
1529
+ renderSectionCard(summary) {
1070
1530
  if (!this.els.map) return;
1071
1531
  this.secCardEl?.remove();
1072
1532
  const priceLabel = summary.priceMin === summary.priceMax ? this.money(summary.priceMin) : `${this.money(summary.priceMin)}\u2013${this.money(summary.priceMax)}`;
1533
+ const leftLabel = (0, import_core2.tCount)("picker.seatsLeftInSection", summary.seatsLeft);
1534
+ const xBtn = `<button type="button" class="sl-seccard-x" aria-label="${(0, import_core2.t)("picker.closeSectionSummary")}">\u2715</button>`;
1073
1535
  const card = document.createElement("div");
1074
- card.className = "sl-seccard on";
1075
- card.setAttribute("role", "dialog");
1076
- card.setAttribute("aria-label", (0, import_core2.t)("picker.sectionSummaryAria", { label: summary.label }));
1077
- const mix = summary.categories.map(
1078
- (c) => `<span class="sl-seccard-mix-item"><span class="sl-seccard-mix-dot" style="background:${c.color}"></span>${c.label} <span class="sl-seccard-mix-price">${this.money(c.price)}</span></span>`
1079
- ).join("");
1080
- card.innerHTML = `<div class="sl-seccard-head"><span class="sl-seccard-dot" style="background:${summary.color}"></span><span class="sl-seccard-name">${summary.label}</span>` + (summary.categories.length ? `<span class="sl-seccard-price">${priceLabel}</span>` : "") + `<button type="button" class="sl-seccard-x" aria-label="${(0, import_core2.t)("picker.closeSectionSummary")}">\u2715</button></div><div class="sl-seccard-zone">${summary.zoneLabel ? `${summary.zoneLabel} \xB7 ` : ""}<span class="sl-seccard-left">${(0, import_core2.tCount)("picker.seatsLeftInSection", summary.seatsLeft)}</span></div>` + (mix ? `<div class="sl-seccard-mix">${mix}</div>` : "") + `<div class="sl-seccard-foot"><button type="button" class="sl-seccard-overview">\u2190 ${(0, import_core2.t)("picker.overview")}</button><span class="sl-seccard-hint">${(0, import_core2.t)("picker.tapSeatHint")}</span></div>`;
1081
- card.querySelector(".sl-seccard-x").addEventListener("click", () => this.controller.overview());
1082
- card.querySelector(".sl-seccard-overview").addEventListener("click", () => this.controller.overview());
1083
- this.els.map.appendChild(card);
1536
+ const narrow = this.root?.dataset.layout === "narrow";
1537
+ if (narrow) {
1538
+ card.className = "sl-seccard strip on";
1539
+ card.setAttribute("role", "status");
1540
+ card.innerHTML = `<span class="sl-seccard-dot" style="background:${summary.color}"></span><span class="sl-seccard-name">${summary.label}</span><span class="sl-seccard-left">${leftLabel}</span>` + (summary.categories.length ? `<span class="sl-seccard-price">${priceLabel}</span>` : "") + xBtn;
1541
+ card.querySelector(".sl-seccard-x").addEventListener("click", () => this.controller.overview());
1542
+ (this.els.sheetHead ?? this.els.side ?? this.els.map).appendChild(card);
1543
+ } else if (this.secCardCollapsed) {
1544
+ card.className = "sl-seccard mini on";
1545
+ card.setAttribute("role", "button");
1546
+ card.setAttribute("aria-label", (0, import_core2.t)("picker.sectionSummaryAria", { label: summary.label }));
1547
+ card.innerHTML = `<span class="sl-seccard-dot" style="background:${summary.color}"></span><span class="sl-seccard-name">${summary.label}</span><span class="sl-seccard-left">${leftLabel}</span>` + xBtn;
1548
+ card.addEventListener("click", (e) => {
1549
+ if (e.target.closest(".sl-seccard-x")) return;
1550
+ this.secCardCollapsed = false;
1551
+ this.secCardShownAt = Date.now();
1552
+ this.renderSectionCard(summary);
1553
+ });
1554
+ card.querySelector(".sl-seccard-x").addEventListener("click", () => this.controller.overview());
1555
+ (this.regions["top-center"] ?? this.els.map).appendChild(card);
1556
+ } else {
1557
+ card.className = "sl-seccard on";
1558
+ card.setAttribute("role", "dialog");
1559
+ card.setAttribute("aria-label", (0, import_core2.t)("picker.sectionSummaryAria", { label: summary.label }));
1560
+ const mix = summary.categories.map((c) => {
1561
+ const dim = this.priceBandKeys != null && !this.priceBandKeys.has(c.key);
1562
+ return `<span class="sl-seccard-mix-item${dim ? " sl-dim" : ""}"><span class="sl-seccard-mix-dot" style="background:${c.color}"></span>${c.label} <span class="sl-seccard-mix-price">${this.money(c.price)}</span></span>`;
1563
+ }).join("");
1564
+ card.innerHTML = `<div class="sl-seccard-head"><span class="sl-seccard-dot" style="background:${summary.color}"></span><span class="sl-seccard-name">${summary.label}</span>` + (summary.categories.length ? `<span class="sl-seccard-price">${priceLabel}</span>` : "") + xBtn + `</div><div class="sl-seccard-zone">${summary.zoneLabel ? `${summary.zoneLabel} \xB7 ` : ""}<span class="sl-seccard-left">${leftLabel}</span></div>` + (mix ? `<div class="sl-seccard-mix">${mix}</div>` : "") + `<div class="sl-seccard-foot"><button type="button" class="sl-seccard-overview">\u2190 ${(0, import_core2.t)("picker.overview")}</button><span class="sl-seccard-hint">${(0, import_core2.t)("picker.tapSeatHint")}</span></div>`;
1565
+ card.querySelector(".sl-seccard-x").addEventListener("click", () => this.controller.overview());
1566
+ card.querySelector(".sl-seccard-overview").addEventListener("click", () => this.controller.overview());
1567
+ (this.regions["top-center"] ?? this.els.map).appendChild(card);
1568
+ }
1084
1569
  this.secCardEl = card;
1085
1570
  }
1571
+ /** Collapse the expanded card to its slim pill (seat-picking started). */
1572
+ collapseSectionCard() {
1573
+ if (!this.secCardEl || this.secCardCollapsed || !this.lastSection) return;
1574
+ if (this.root?.dataset.layout === "narrow") return;
1575
+ this.secCardCollapsed = true;
1576
+ this.renderSectionCard(this.lastSection);
1577
+ }
1578
+ /**
1579
+ * onViewChange hook for the card. The focus glide's own settle (within the
1580
+ * grace window) enforces the ~25% coverage rule with the FINAL viewport; any
1581
+ * later pan/zoom means seat-picking has begun → collapse to the pill.
1582
+ */
1583
+ sectionCardOnView() {
1584
+ if (!this.secCardEl || this.secCardCollapsed || !this.lastSection) return;
1585
+ if (this.root?.dataset.layout === "narrow") return;
1586
+ if (Date.now() - this.secCardShownAt < 1400) {
1587
+ if (this.sectionCardCoverage() > 0.25) this.collapseSectionCard();
1588
+ return;
1589
+ }
1590
+ this.collapseSectionCard();
1591
+ }
1592
+ /** Fraction of the focused section's on-screen bbox covered by the card. */
1593
+ sectionCardCoverage() {
1594
+ const card = this.secCardEl;
1595
+ const sec = this.lastSection;
1596
+ if (!card || !sec || !this.els.map) return 0;
1597
+ const outline = this.activeFloorObjects().find((o) => o.type === "section" && o.id === sec.id)?.outline;
1598
+ if (!outline || outline.length < 3) return 0;
1599
+ const pts = outline.map((p) => this.controller.worldToScreen(p));
1600
+ const xs = pts.map((p) => p.x);
1601
+ const ys = pts.map((p) => p.y);
1602
+ const bx = Math.min(...xs);
1603
+ const by = Math.min(...ys);
1604
+ const bw = Math.max(...xs) - bx;
1605
+ const bh = Math.max(...ys) - by;
1606
+ if (bw <= 0 || bh <= 0) return 0;
1607
+ const mapR = this.els.map.getBoundingClientRect();
1608
+ const cr = card.getBoundingClientRect();
1609
+ const cx = cr.left - mapR.left;
1610
+ const cy = cr.top - mapR.top;
1611
+ const ox = Math.max(0, Math.min(cx + cr.width, bx + bw) - Math.max(cx, bx));
1612
+ const oy = Math.max(0, Math.min(cy + cr.height, by + bh) - Math.max(cy, by));
1613
+ return ox * oy / (bw * bh);
1614
+ }
1086
1615
  /** aria-live readout when keyboard focus lands on a seat. */
1087
1616
  announceSeat(seat) {
1088
1617
  if (!this.srEl) return;
@@ -1257,7 +1786,8 @@ var SeatPicker = class _SeatPicker {
1257
1786
  const left = this.controller.categoryAvailability();
1258
1787
  this.els.prices.innerHTML = doc.categories.map((c) => {
1259
1788
  const price = c.tiers?.length ? c.tiers[0].price : c.price;
1260
- return `<div class="sl-price-row" data-cat="${c.key}"><span class="sl-dot" style="background:${c.color}"></span><span class="sl-price-label">${c.label}</span><span class="sl-price-left">${left[c.key] ?? 0} left</span>` + (price != null ? `<span class="sl-price-amt">${this.money(price)}</span>` : "") + `</div>`;
1789
+ const dim = this.priceBandKeys != null && !this.priceBandKeys.has(c.key);
1790
+ return `<div class="sl-price-row${dim ? " sl-dim" : ""}" data-cat="${c.key}"><span class="sl-dot" style="background:${c.color}"></span><span class="sl-price-label">${c.label}</span><span class="sl-price-left">${left[c.key] ?? 0} left</span>` + (price != null ? `<span class="sl-price-amt">${this.money(price)}</span>` : "") + `</div>`;
1261
1791
  }).join("");
1262
1792
  this.els.prices.querySelectorAll(".sl-price-row").forEach((row) => {
1263
1793
  row.addEventListener("mouseenter", () => this.controller.getRenderer()?.setCategoryHighlight?.(row.dataset.cat ?? null));
@@ -1363,6 +1893,18 @@ var SeatPicker = class _SeatPicker {
1363
1893
  const cta = this.els.cta;
1364
1894
  cta.disabled = count === 0;
1365
1895
  cta.textContent = this.hold ? "Continue to checkout" : count ? "Hold seats & checkout" : "Select seats";
1896
+ if (this.els.peek) {
1897
+ if (count) {
1898
+ this.els.peek.innerHTML = `<span>${count} ${count === 1 ? "ticket" : "tickets"} \xB7 ${this.money(total)}</span><span class="go">${this.hold ? "Continue \u2192" : "Review \u2192"}</span>`;
1899
+ } else {
1900
+ const prices = (this.controller.doc?.categories ?? []).map((c) => this.catPrice(c)).filter((p) => p != null);
1901
+ this.els.peek.innerHTML = (prices.length ? `<span>From ${this.money(Math.min(...prices))}</span>` : "<span>Pick your seats</span>") + `<span class="sub">\xB7 Best available</span><span class="go">\u2191</span>`;
1902
+ }
1903
+ }
1904
+ if (this.root?.dataset.layout === "narrow" && count > 0 && this.lastTrayCount === 0) {
1905
+ this.root.dataset.sheet = "open";
1906
+ }
1907
+ this.lastTrayCount = count;
1366
1908
  this.opts.onSelectionChange?.(seats);
1367
1909
  }
1368
1910
  async handleCta() {