@vio-live/web-sdk 0.26.1 → 0.28.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.
@@ -1,5 +1,5 @@
1
- import { Vio, __decorateClass, primaryImageUrl, displayPrice, toCampaignId, getGlobalCurrency, ensureVippsButton, formatPrice, buyerRefOf, nexiReturnPaymentId, readNexiPending, kustomReturnOutcome, KUSTOM_RETURN_QUERY_KEYS, ADYEN_RETURN_METHOD, adyenReturnParams, ADYEN_RETURN_QUERY_KEYS, adyenReturnOutcome, gatewayHasBuyerRef, AdyenOriginError, normalizeSlotId, autoSlotDisplayName, autoSlotId, normalizeScope, campaignCandidates } from './chunk-NXIYIY2D.js';
2
- import { LitElement, css, html } from 'lit';
1
+ import { Vio, __decorateClass, primaryImageUrl, displayPrice, toCampaignId, getGlobalCurrency, ensureVippsButton, formatPrice, buyerRefOf, nexiReturnPaymentId, readNexiPending, kustomReturnOutcome, KUSTOM_RETURN_QUERY_KEYS, ADYEN_RETURN_METHOD, adyenReturnParams, ADYEN_RETURN_QUERY_KEYS, adyenReturnOutcome, gatewayHasBuyerRef, rememberedCheckoutSponsor, checkoutSponsorOf, AdyenOriginError, normalizeSlotId, autoSlotDisplayName, autoSlotId, sponsorIdentityFrom, Configuration, normalizeScope, campaignCandidates } from './chunk-AFJ5M53F.js';
2
+ import { LitElement, css, html, nothing } from 'lit';
3
3
  import { property, state, query } from 'lit/decorators.js';
4
4
 
5
5
  // src/ui/tokens.ts
@@ -151,7 +151,38 @@ var THEME_KEY_TO_CSS_VAR = {
151
151
  spaceSm: "--vio-space-sm",
152
152
  spaceMd: "--vio-space-md",
153
153
  spaceLg: "--vio-space-lg",
154
- spaceXl: "--vio-space-xl"
154
+ spaceXl: "--vio-space-xl",
155
+ detailWidth: "--vio-detail-width",
156
+ detailPaddingX: "--vio-detail-padding-x",
157
+ detailLogoHeight: "--vio-detail-logo-height",
158
+ detailLogoZoom: "--vio-detail-logo-zoom",
159
+ detailTitleSize: "--vio-detail-title-size",
160
+ detailPriceSize: "--vio-detail-price-size",
161
+ detailImageFit: "--vio-detail-image-fit",
162
+ detailImageAspect: "--vio-detail-image-aspect",
163
+ detailImageMaxHeight: "--vio-detail-image-max-height",
164
+ detailImageZoom: "--vio-detail-image-zoom",
165
+ detailImageInset: "--vio-detail-image-inset",
166
+ detailImageBg: "--vio-detail-image-bg",
167
+ detailThumbSize: "--vio-detail-thumb-size",
168
+ detailThumbGap: "--vio-detail-thumb-gap",
169
+ detailThumbRadius: "--vio-detail-thumb-radius",
170
+ detailThumbActiveColor: "--vio-detail-thumb-active-color",
171
+ detailOptionHeight: "--vio-detail-option-height",
172
+ detailOptionGap: "--vio-detail-option-gap",
173
+ detailOptionRadius: "--vio-detail-option-radius",
174
+ detailOptionWrap: "--vio-detail-option-wrap",
175
+ detailButtonHeight: "--vio-detail-button-height",
176
+ detailButtonRadius: "--vio-detail-button-radius",
177
+ detailBuyPosition: "--vio-detail-buy-position",
178
+ detailBuyShadow: "--vio-detail-buy-shadow",
179
+ cartWidth: "--vio-cart-width",
180
+ checkoutWidth: "--vio-checkout-width",
181
+ confirmLogoHeight: "--vio-confirm-logo-height",
182
+ confirmTitleSize: "--vio-confirm-title-size",
183
+ confirmCheckBg: "--vio-confirm-check-bg",
184
+ redirectBg: "--vio-redirect-bg",
185
+ redirectSpinner: "--vio-redirect-spinner"
155
186
  };
156
187
  function applyVioTheme(overrides, target = typeof document !== "undefined" ? document.documentElement : null) {
157
188
  if (!target) return;
@@ -705,6 +736,54 @@ __decorateClass([
705
736
  __decorateClass([
706
737
  state()
707
738
  ], VioProduct.prototype, "justAdded", 2);
739
+ function resolveSponsorIdentity(sponsorId, checkoutId) {
740
+ try {
741
+ if (typeof Vio.sponsorIdentity === "function") return Vio.sponsorIdentity(sponsorId, { checkoutId });
742
+ return sponsorIdentityFrom(Vio.bootstrapCache, sponsorId);
743
+ } catch {
744
+ return null;
745
+ }
746
+ }
747
+ function sponsorDetail(sponsorId, checkoutId) {
748
+ const sponsor = checkoutSponsorOf(resolveSponsorIdentity(sponsorId, checkoutId));
749
+ return sponsor ? { sponsor } : {};
750
+ }
751
+ var failedLogos = /* @__PURE__ */ new Set();
752
+ function usableLogo(identity) {
753
+ const url = identity?.logoUrl;
754
+ return url && !failedLogos.has(url) ? url : null;
755
+ }
756
+ function renderSponsorMark(host, identity, classes) {
757
+ if (!identity) return nothing;
758
+ const logo = usableLogo(identity);
759
+ if (logo) {
760
+ return html`<img
761
+ class=${classes.logo}
762
+ src=${logo}
763
+ alt=${identity.name}
764
+ decoding="async"
765
+ @error=${() => {
766
+ failedLogos.add(logo);
767
+ host.requestUpdate();
768
+ }}
769
+ />`;
770
+ }
771
+ return identity.name ? html`<span class=${classes.name}>${identity.name}</span>` : nothing;
772
+ }
773
+ var INIT_WAIT_MS = 1e4;
774
+ async function whenBootstrapped() {
775
+ try {
776
+ if (Vio.bootstrapCache) return true;
777
+ if (!Configuration.isInitialized) await Configuration.whenReady(INIT_WAIT_MS);
778
+ if (!Configuration.isInitialized || typeof Vio.bootstrap !== "function") return false;
779
+ await Vio.bootstrap();
780
+ return !!Vio.bootstrapCache;
781
+ } catch {
782
+ return false;
783
+ }
784
+ }
785
+
786
+ // src/ui/components/vio-product-carousel.ts
708
787
  var VioProductCarousel = class extends LitElement {
709
788
  constructor() {
710
789
  super(...arguments);
@@ -1077,7 +1156,7 @@ var VioProductCarousel = class extends LitElement {
1077
1156
  if (this.fetched && this.fetched.length > 0) {
1078
1157
  return html`${this.fetched.map(({ product: p, sponsorId }) => {
1079
1158
  const img = primaryImageUrl(p) ?? "";
1080
- const retailer = sponsorNameFromBootstrap(sponsorId) ?? p.brand ?? "";
1159
+ const retailer = resolveSponsorIdentity(sponsorId)?.name || p.brand || "";
1081
1160
  return html`
1082
1161
  <vio-product
1083
1162
  product-id=${String(p.id)}
@@ -1195,11 +1274,101 @@ __decorateClass([
1195
1274
  __decorateClass([
1196
1275
  state()
1197
1276
  ], VioProductCarousel.prototype, "fetchError", 2);
1198
- function sponsorNameFromBootstrap(sponsorId) {
1199
- const boot = Vio.bootstrapCache;
1200
- if (!boot) return void 0;
1201
- if (boot.primarySponsor?.id === sponsorId) return boot.primarySponsor.name;
1202
- return boot.secondarySponsors?.find((s) => s.id === sponsorId)?.name;
1277
+
1278
+ // src/ui/payment-redirect.ts
1279
+ var OVERLAY_CLASS = "vio-redirect-overlay";
1280
+ var STYLE_ID = "vio-redirect-overlay-style";
1281
+ var LABEL = { vipps: "Vipps", stripe: "Stripe", klarna: "Klarna" };
1282
+ function emit(name, detail, cancelable = false) {
1283
+ if (typeof document === "undefined") return true;
1284
+ return document.dispatchEvent(new CustomEvent(name, { detail, cancelable }));
1285
+ }
1286
+ function injectStyle() {
1287
+ if (document.getElementById(STYLE_ID)) return;
1288
+ const style = document.createElement("style");
1289
+ style.id = STYLE_ID;
1290
+ style.textContent = `
1291
+ .${OVERLAY_CLASS} {
1292
+ position: fixed; inset: 0; z-index: 2147483000;
1293
+ display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 16px;
1294
+ background: var(--vio-redirect-bg, rgba(255, 255, 255, 0.94));
1295
+ color: var(--vio-redirect-color, var(--vio-color-text, #0a0a0a));
1296
+ font: 600 16px/1.3 var(--vio-redirect-font, var(--vio-font-sans, -apple-system, sans-serif));
1297
+ opacity: 0; transition: opacity 0.18s ease; cursor: progress;
1298
+ }
1299
+ .${OVERLAY_CLASS}.is-shown { opacity: 1; }
1300
+ .${OVERLAY_CLASS} .vio-redirect-spinner {
1301
+ width: 36px; height: 36px; border-radius: 50%;
1302
+ border: 3px solid rgba(0, 0, 0, 0.1); border-top-color: var(--vio-redirect-spinner, #ff5b24);
1303
+ animation: vio-redirect-spin 0.8s linear infinite;
1304
+ }
1305
+ @keyframes vio-redirect-spin { to { transform: rotate(360deg); } }
1306
+ @media (prefers-reduced-motion: reduce) {
1307
+ .${OVERLAY_CLASS} { transition: none; }
1308
+ .${OVERLAY_CLASS} .vio-redirect-spinner { animation: none; }
1309
+ }`;
1310
+ document.head.appendChild(style);
1311
+ }
1312
+ var current = null;
1313
+ function removeOverlay() {
1314
+ const el = current?.el;
1315
+ if (!el) return;
1316
+ el.classList.remove("is-shown");
1317
+ setTimeout(() => el.remove(), 200);
1318
+ current.el = null;
1319
+ }
1320
+ function onPageShow(e) {
1321
+ if (!e.persisted || !current || current.settled) return;
1322
+ current.settled = true;
1323
+ removeOverlay();
1324
+ emit("vio:payment-redirect-cancel", { ...current.detail, reason: "back" });
1325
+ current = null;
1326
+ }
1327
+ function beginPaymentRedirect(detail) {
1328
+ if (current && !current.settled) {
1329
+ current.settled = true;
1330
+ removeOverlay();
1331
+ }
1332
+ const hostTookOver = !emit("vio:payment-redirect-start", detail, true);
1333
+ const state6 = { el: null, detail, settled: false };
1334
+ current = state6;
1335
+ if (!hostTookOver && typeof document !== "undefined" && document.body) {
1336
+ injectStyle();
1337
+ const el = document.createElement("div");
1338
+ el.className = OVERLAY_CLASS;
1339
+ el.setAttribute("role", "status");
1340
+ el.setAttribute("aria-live", "polite");
1341
+ const spinner = document.createElement("div");
1342
+ spinner.className = "vio-redirect-spinner";
1343
+ spinner.setAttribute("aria-hidden", "true");
1344
+ const text = document.createElement("div");
1345
+ text.className = "vio-redirect-text";
1346
+ text.textContent = `\xC5pner ${LABEL[detail.method] ?? detail.method} \u2026`;
1347
+ el.append(spinner, text);
1348
+ el.addEventListener("click", (e) => e.stopPropagation());
1349
+ document.body.appendChild(el);
1350
+ requestAnimationFrame(() => el.classList.add("is-shown"));
1351
+ state6.el = el;
1352
+ }
1353
+ if (typeof window !== "undefined") window.addEventListener("pageshow", onPageShow);
1354
+ const settle = (event, reason) => {
1355
+ if (state6.settled) return;
1356
+ state6.settled = true;
1357
+ if (current === state6) {
1358
+ removeOverlay();
1359
+ current = null;
1360
+ } else if (state6.el) {
1361
+ state6.el.remove();
1362
+ }
1363
+ emit(event, { ...detail, reason });
1364
+ };
1365
+ return {
1366
+ fail: (reason) => settle("vio:payment-redirect-error", reason),
1367
+ abort: (reason = "not-started") => settle("vio:payment-redirect-cancel", reason)
1368
+ };
1369
+ }
1370
+ function returnCancelled(detail) {
1371
+ emit("vio:payment-redirect-cancel", { ...detail, reason: "cancelled" });
1203
1372
  }
1204
1373
 
1205
1374
  // src/core/checkout/method-taxonomy.ts
@@ -1409,6 +1578,207 @@ function isWithin(node, ancestor) {
1409
1578
  return false;
1410
1579
  }
1411
1580
 
1581
+ // src/ui/bottom-sheet.ts
1582
+ var PHONE = "(max-width: 600px)";
1583
+ var SLOP = 6;
1584
+ var CLOSE_DISTANCE = 120;
1585
+ var FLICK_VELOCITY = 0.5;
1586
+ var FLICK_MIN = 24;
1587
+ var CLICK_AFTER_DRAG_MS = 400;
1588
+ function isPhoneSheet() {
1589
+ return typeof window !== "undefined" && typeof window.matchMedia === "function" && window.matchMedia(PHONE).matches;
1590
+ }
1591
+ var BottomSheet = class {
1592
+ constructor(opts) {
1593
+ this.opts = opts;
1594
+ this.active = false;
1595
+ this.locked = false;
1596
+ this.drag = null;
1597
+ this.dragEndedAt = -Infinity;
1598
+ this.onPointerDown = (e) => {
1599
+ if (!e.isPrimary || e.pointerType === "mouse" && e.button !== 0) return;
1600
+ if (this.drag || !isPhoneSheet()) return;
1601
+ const sheet = this.opts.sheet();
1602
+ if (!sheet || !inZone(e, sheet, this.opts.dragZones)) return;
1603
+ this.drag = {
1604
+ pointerId: e.pointerId,
1605
+ startY: e.clientY,
1606
+ lastY: e.clientY,
1607
+ lastT: e.timeStamp,
1608
+ velocity: 0,
1609
+ moving: false,
1610
+ offset: 0
1611
+ };
1612
+ window.addEventListener("pointermove", this.onPointerMove);
1613
+ window.addEventListener("pointerup", this.onPointerUp);
1614
+ window.addEventListener("pointercancel", this.onPointerCancel);
1615
+ };
1616
+ this.onPointerMove = (e) => {
1617
+ const drag = this.drag;
1618
+ if (!drag || e.pointerId !== drag.pointerId) return;
1619
+ const dy = e.clientY - drag.startY;
1620
+ if (!drag.moving) {
1621
+ if (dy < -SLOP) return this.abandon();
1622
+ if (dy <= SLOP) return;
1623
+ drag.moving = true;
1624
+ const sheet2 = this.opts.sheet();
1625
+ if (sheet2) sheet2.style.transition = "none";
1626
+ const backdrop2 = this.opts.backdrop?.();
1627
+ if (backdrop2) backdrop2.style.transition = "none";
1628
+ }
1629
+ const dt = e.timeStamp - drag.lastT;
1630
+ if (dt > 0) drag.velocity = 0.8 * ((e.clientY - drag.lastY) / dt) + 0.2 * drag.velocity;
1631
+ drag.lastY = e.clientY;
1632
+ drag.lastT = e.timeStamp;
1633
+ drag.offset = Math.max(0, dy);
1634
+ const sheet = this.opts.sheet();
1635
+ if (sheet) sheet.style.transform = `translateY(${drag.offset}px)`;
1636
+ const backdrop = this.opts.backdrop?.();
1637
+ if (backdrop && sheet) {
1638
+ const height = sheet.getBoundingClientRect().height || window.innerHeight;
1639
+ backdrop.style.opacity = String(Math.max(0, 1 - drag.offset / height));
1640
+ }
1641
+ if (e.cancelable) e.preventDefault();
1642
+ };
1643
+ this.onPointerUp = (e) => {
1644
+ const drag = this.drag;
1645
+ if (!drag || e.pointerId !== drag.pointerId) return;
1646
+ this.stopTracking();
1647
+ this.drag = null;
1648
+ if (!drag.moving) return;
1649
+ this.dragEndedAt = performance.now();
1650
+ const sheet = this.opts.sheet();
1651
+ const height = sheet?.getBoundingClientRect().height || window.innerHeight;
1652
+ const far = drag.offset >= Math.min(CLOSE_DISTANCE, height / 4);
1653
+ const flick = drag.velocity >= FLICK_VELOCITY && drag.offset >= FLICK_MIN;
1654
+ const allowed = !this.opts.canClose || this.opts.canClose();
1655
+ this.release();
1656
+ if ((far || flick) && allowed) {
1657
+ if (sheet) sheet.style.transform = "translateY(100%)";
1658
+ this.opts.close();
1659
+ } else if (sheet) {
1660
+ sheet.style.transform = "";
1661
+ }
1662
+ };
1663
+ this.onPointerCancel = (e) => {
1664
+ if (!this.drag || e.pointerId !== this.drag.pointerId) return;
1665
+ this.abandon();
1666
+ };
1667
+ /** The press that ended a drag also fires a click on ✕ or the grab bar: swallow it. */
1668
+ this.onClickCapture = (e) => {
1669
+ if (performance.now() - this.dragEndedAt > CLICK_AFTER_DRAG_MS) return;
1670
+ this.dragEndedAt = -Infinity;
1671
+ e.stopPropagation();
1672
+ e.preventDefault();
1673
+ };
1674
+ }
1675
+ /** Call from `updated()` when `open` changed. Same value twice is a no-op. */
1676
+ update(open) {
1677
+ if (open === this.active) return;
1678
+ this.active = open;
1679
+ if (open) this.opened();
1680
+ else this.closed();
1681
+ }
1682
+ /** Call from `disconnectedCallback()`: a sheet torn down while open must not keep the page locked. */
1683
+ disconnect() {
1684
+ if (this.active) {
1685
+ this.active = false;
1686
+ this.closed();
1687
+ }
1688
+ }
1689
+ opened() {
1690
+ if (isPhoneSheet()) {
1691
+ lockPageScroll();
1692
+ this.locked = true;
1693
+ }
1694
+ this.opts.host.addEventListener("pointerdown", this.onPointerDown);
1695
+ this.opts.host.addEventListener("click", this.onClickCapture, true);
1696
+ }
1697
+ closed() {
1698
+ this.opts.host.removeEventListener("pointerdown", this.onPointerDown);
1699
+ this.opts.host.removeEventListener("click", this.onClickCapture, true);
1700
+ this.stopTracking();
1701
+ this.drag = null;
1702
+ const sheet = this.opts.sheet();
1703
+ if (sheet) {
1704
+ sheet.style.transform = "";
1705
+ sheet.style.transition = "";
1706
+ }
1707
+ const backdrop = this.opts.backdrop?.();
1708
+ if (backdrop) {
1709
+ backdrop.style.opacity = "";
1710
+ backdrop.style.transition = "";
1711
+ }
1712
+ if (this.locked) {
1713
+ this.locked = false;
1714
+ unlockPageScroll();
1715
+ }
1716
+ }
1717
+ /** Give up on this press and put the sheet back where it was. */
1718
+ abandon() {
1719
+ const moving = this.drag?.moving;
1720
+ this.stopTracking();
1721
+ this.drag = null;
1722
+ if (!moving) return;
1723
+ this.release();
1724
+ const sheet = this.opts.sheet();
1725
+ if (sheet) sheet.style.transform = "";
1726
+ }
1727
+ /** Hand the sheet and backdrop back to their CSS transitions. */
1728
+ release() {
1729
+ const sheet = this.opts.sheet();
1730
+ if (sheet) sheet.style.transition = "";
1731
+ const backdrop = this.opts.backdrop?.();
1732
+ if (backdrop) {
1733
+ backdrop.style.transition = "";
1734
+ backdrop.style.opacity = "";
1735
+ }
1736
+ }
1737
+ stopTracking() {
1738
+ window.removeEventListener("pointermove", this.onPointerMove);
1739
+ window.removeEventListener("pointerup", this.onPointerUp);
1740
+ window.removeEventListener("pointercancel", this.onPointerCancel);
1741
+ }
1742
+ };
1743
+ function inZone(e, sheet, zones) {
1744
+ for (const node of e.composedPath()) {
1745
+ if (node === sheet) return false;
1746
+ if (node instanceof Element && node.matches(zones)) return sheet.contains(node);
1747
+ }
1748
+ return false;
1749
+ }
1750
+ var locks = 0;
1751
+ var saved = null;
1752
+ var LOCKED = ["position", "top", "left", "right", "width", "overflow"];
1753
+ function lockPageScroll() {
1754
+ if (typeof document === "undefined") return;
1755
+ locks += 1;
1756
+ if (locks > 1) return;
1757
+ const body = document.body;
1758
+ const scrollY = window.scrollY || document.documentElement.scrollTop || 0;
1759
+ saved = { scrollY, body: {} };
1760
+ for (const p of LOCKED) saved.body[p] = body.style[p];
1761
+ body.style.position = "fixed";
1762
+ body.style.top = `-${scrollY}px`;
1763
+ body.style.left = "0";
1764
+ body.style.right = "0";
1765
+ body.style.width = "100%";
1766
+ body.style.overflow = "hidden";
1767
+ }
1768
+ function unlockPageScroll() {
1769
+ if (typeof document === "undefined" || locks === 0) return;
1770
+ locks -= 1;
1771
+ if (locks > 0 || !saved) return;
1772
+ const { scrollY, body } = saved;
1773
+ saved = null;
1774
+ for (const p of LOCKED) document.body.style[p] = body[p] ?? "";
1775
+ const root = document.documentElement;
1776
+ const behavior = root.style.scrollBehavior;
1777
+ root.style.scrollBehavior = "auto";
1778
+ window.scrollTo(0, scrollY);
1779
+ root.style.scrollBehavior = behavior;
1780
+ }
1781
+
1412
1782
  // src/ui/components/vio-product-detail.ts
1413
1783
  var STRIPE_LOGO_SRC = "data:image/svg+xml;utf8," + encodeURIComponent(
1414
1784
  '<svg width="360" height="150" viewBox="0 0 360 150" fill="none" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" clip-rule="evenodd" d="M360 77.4001C360 51.8001 347.6 31.6001 323.9 31.6001C300.1 31.6001 285.7 51.8001 285.7 77.2001C285.7 107.3 302.7 122.5 327.1 122.5C339 122.5 348 119.8 354.8 116V96.0001C348 99.4001 340.2 101.5 330.3 101.5C320.6 101.5 312 98.1001 310.9 86.3001H359.8C359.8 85.0001 360 79.8001 360 77.4001ZM310.6 67.9001C310.6 56.6001 317.5 51.9001 323.8 51.9001C329.9 51.9001 336.4 56.6001 336.4 67.9001H310.6Z" fill="#533AFD"/><path fill-rule="evenodd" clip-rule="evenodd" d="M247.1 31.6001C237.3 31.6001 231 36.2001 227.5 39.4001L226.2 33.2001H204.2V149.8L229.2 144.5L229.3 116.2C232.9 118.8 238.2 122.5 247 122.5C264.9 122.5 281.2 108.1 281.2 76.4001C281.1 47.4001 264.6 31.6001 247.1 31.6001ZM241.1 100.5C235.2 100.5 231.7 98.4001 229.3 95.8001L229.2 58.7001C231.8 55.8001 235.4 53.8001 241.1 53.8001C250.2 53.8001 256.5 64.0001 256.5 77.1001C256.5 90.5001 250.3 100.5 241.1 100.5Z" fill="#533AFD"/><path fill-rule="evenodd" clip-rule="evenodd" d="M169.8 25.7L194.9 20.3V0L169.8 5.3V25.7Z" fill="#533AFD"/><path d="M194.9 33.3H169.8V120.8H194.9V33.3Z" fill="#533AFD"/><path fill-rule="evenodd" clip-rule="evenodd" d="M142.9 40.7L141.3 33.3H119.7V120.8H144.7V61.5C150.6 53.8 160.6 55.2 163.7 56.3V33.3C160.5 32.1 148.8 29.9 142.9 40.7Z" fill="#533AFD"/><path fill-rule="evenodd" clip-rule="evenodd" d="M92.8999 11.6001L68.4999 16.8001L68.3999 96.9001C68.3999 111.7 79.4999 122.6 94.2999 122.6C102.5 122.6 108.5 121.1 111.8 119.3V99.0001C108.6 100.3 92.7999 104.9 92.7999 90.1001V54.6001H111.8V33.3001H92.7999L92.8999 11.6001Z" fill="#533AFD"/><path fill-rule="evenodd" clip-rule="evenodd" d="M25.3 58.7001C25.3 54.8001 28.5 53.3001 33.8 53.3001C41.4 53.3001 51 55.6001 58.6 59.7001V36.2001C50.3 32.9001 42.1 31.6001 33.8 31.6001C13.5 31.6001 0 42.2001 0 59.9001C0 87.5001 38 83.1001 38 95.0001C38 99.6001 34 101.1 28.4 101.1C20.1 101.1 9.5 97.7001 1.1 93.1001V116.9C10.4 120.9 19.8 122.6 28.4 122.6C49.2 122.6 63.5 112.3 63.5 94.4001C63.4 64.6001 25.3 69.9001 25.3 58.7001Z" fill="#533AFD"/></svg>'
@@ -1443,6 +1813,10 @@ var VioProductDetail = class extends LitElement {
1443
1813
  this.selectedOptions = {};
1444
1814
  this.quantity = 1;
1445
1815
  this.activeImageIndex = 0;
1816
+ this.thumbsOverflow = false;
1817
+ this.thumbsAtStart = true;
1818
+ this.thumbsAtEnd = false;
1819
+ this.logoSquare = false;
1446
1820
  this.adding = false;
1447
1821
  this.applePayOk = false;
1448
1822
  this.availableMethods = null;
@@ -1460,6 +1834,14 @@ var VioProductDetail = class extends LitElement {
1460
1834
  container: () => this.renderRoot.querySelector(".modal"),
1461
1835
  close: () => this.close()
1462
1836
  });
1837
+ /** Phones: the page behind is locked, and the sheet closes by pulling the bar or the top bar down. */
1838
+ this.bottomSheet = new BottomSheet({
1839
+ host: this,
1840
+ sheet: () => this.renderRoot.querySelector(".modal"),
1841
+ backdrop: () => this.renderRoot.querySelector(".backdrop"),
1842
+ dragZones: ".handle, .topbar",
1843
+ close: () => this.close()
1844
+ });
1463
1845
  this.boundProductClick = (e) => {
1464
1846
  if (!this.autoOpen) return;
1465
1847
  const ev = e;
@@ -1487,6 +1869,14 @@ var VioProductDetail = class extends LitElement {
1487
1869
  this.show();
1488
1870
  void this.fetchProduct();
1489
1871
  };
1872
+ this.onThumbsScroll = () => {
1873
+ const row = this.thumbRow();
1874
+ if (!row) return;
1875
+ const atStart = row.scrollLeft <= 1;
1876
+ const atEnd = row.scrollLeft + row.clientWidth >= row.scrollWidth - 1;
1877
+ if (atStart !== this.thumbsAtStart) this.thumbsAtStart = atStart;
1878
+ if (atEnd !== this.thumbsAtEnd) this.thumbsAtEnd = atEnd;
1879
+ };
1490
1880
  }
1491
1881
  /** The card's context applies to the product it opened — not to one the host opened by hand later. */
1492
1882
  get fromCard() {
@@ -1574,7 +1964,10 @@ var VioProductDetail = class extends LitElement {
1574
1964
  height: 92vh;
1575
1965
  border-radius: var(--vio-radius-xl, 16px) var(--vio-radius-xl, 16px) 0 0;
1576
1966
  box-shadow: 0 -8px 24px rgba(0, 0, 0, 0.08);
1967
+ overscroll-behavior: contain;
1577
1968
  }
1969
+ /* Pulled down to close (bottom-sheet.ts): the browser must not take the gesture for a scroll. */
1970
+ .handle, .topbar { touch-action: none; }
1578
1971
  .handle {
1579
1972
  display: flex;
1580
1973
  justify-content: center;
@@ -1595,8 +1988,8 @@ var VioProductDetail = class extends LitElement {
1595
1988
  @media (min-width: 601px) {
1596
1989
  .modal {
1597
1990
  inset: 0 0 0 auto;
1598
- width: min(440px, 100%);
1599
- max-width: 440px;
1991
+ width: min(var(--vio-detail-width, 440px), 100%);
1992
+ max-width: var(--vio-detail-width, 440px);
1600
1993
  border-radius: 0;
1601
1994
  box-shadow: -8px 0 24px rgba(0, 0, 0, 0.1);
1602
1995
  transform: translateX(100%);
@@ -1610,29 +2003,59 @@ var VioProductDetail = class extends LitElement {
1610
2003
  max-width: none;
1611
2004
  grid-template-columns: 1fr;
1612
2005
  gap: 20px;
1613
- padding: 20px 24px 40px;
2006
+ padding: var(--vio-detail-padding-top, 0px) var(--vio-detail-padding-x, var(--vio-space-lg, 24px)) 40px;
1614
2007
  }
1615
- .modal .name { font-size: 26px; line-height: 1.2; }
1616
- .modal .gallery-main { width: 100%; }
2008
+ .modal .name { font-size: var(--vio-detail-title-size, 26px); line-height: 1.2; }
2009
+ .modal .gallery-main { width: auto; }
1617
2010
  }
1618
2011
 
1619
2012
  .topbar {
1620
2013
  display: flex;
1621
2014
  justify-content: space-between;
1622
2015
  align-items: center;
1623
- padding: 20px var(--vio-space-lg, 24px);
2016
+ gap: 16px;
2017
+ min-height: var(--vio-detail-topbar-height, 64px);
2018
+ box-sizing: border-box;
2019
+ padding: 10px var(--vio-detail-padding-x, var(--vio-space-lg, 24px));
1624
2020
  position: sticky;
1625
2021
  top: 0;
1626
2022
  background: var(--vio-color-surface, #fff);
1627
2023
  border-bottom: 1px solid var(--vio-color-border, #e5e5e5);
1628
2024
  z-index: 1;
1629
2025
  }
1630
- .topbar-brand {
1631
- font-size: 12px;
2026
+ /* Who sells it, top left beside the close: the sponsor's logo, else its name. */
2027
+ .topbar-sponsor { flex: 1 1 auto; display: flex; align-items: center; gap: 10px; min-width: 0; }
2028
+ /* The logo box clips, so --vio-detail-logo-zoom can crop the white a
2029
+ square file carries around its mark. */
2030
+ .topbar-logo-box {
2031
+ flex: 0 0 auto;
2032
+ display: flex;
2033
+ align-items: center;
2034
+ height: var(--vio-detail-logo-height, 44px);
2035
+ max-width: var(--vio-detail-logo-max-width, 220px);
2036
+ overflow: hidden;
2037
+ }
2038
+ .topbar-logo {
2039
+ display: block;
2040
+ width: auto;
2041
+ height: 100%;
2042
+ max-width: var(--vio-detail-logo-max-width, 220px);
2043
+ object-fit: contain;
2044
+ object-position: left center;
2045
+ transform: scale(var(--vio-detail-logo-zoom, 1));
2046
+ transform-origin: center;
2047
+ }
2048
+ /* A square (or near) logo is a mark, not a wordmark: say whose it is. */
2049
+ .topbar-sponsor:not(.is-square) .topbar-mark-name { display: none; }
2050
+ .topbar-name {
2051
+ font-size: var(--vio-detail-logo-name-size, 13px);
1632
2052
  text-transform: uppercase;
1633
2053
  letter-spacing: var(--vio-tracking-label, 0.1em);
1634
2054
  color: var(--vio-color-text-secondary, #666);
1635
- font-weight: 500;
2055
+ font-weight: 600;
2056
+ white-space: nowrap;
2057
+ overflow: hidden;
2058
+ text-overflow: ellipsis;
1636
2059
  }
1637
2060
  .close {
1638
2061
  background: none;
@@ -1658,51 +2081,110 @@ var VioProductDetail = class extends LitElement {
1658
2081
  gap: 64px;
1659
2082
  }
1660
2083
  @media (max-width: 800px) {
1661
- .body { grid-template-columns: 1fr; gap: 32px; padding: 24px var(--vio-space-lg, 24px) 64px; }
2084
+ .body {
2085
+ grid-template-columns: 1fr;
2086
+ gap: 24px;
2087
+ padding: var(--vio-detail-padding-top, 0px) var(--vio-detail-padding-x, var(--vio-space-lg, 24px)) 64px;
2088
+ }
1662
2089
  }
1663
2090
 
1664
- /* Gallery */
1665
- .gallery { display: flex; flex-direction: column; gap: 16px; }
2091
+ /* Gallery. One fixed proportion for every image (4:5 by default) that the
2092
+ image fills (cover): no white bars, and the panel never jumps between
2093
+ photos of different shapes (Angelo, 2026-10-22). Its side margin is
2094
+ --vio-detail-image-inset (the panel's padding by default; 0 = edge to
2095
+ edge). Fit, proportion, a height cap and a zoom (to crop a white margin
2096
+ baked into the file) are the host's to set. */
2097
+ .gallery { display: flex; flex-direction: column; gap: var(--vio-detail-gallery-gap, 12px); min-width: 0; }
1666
2098
  .gallery-main {
1667
- aspect-ratio: 1;
1668
- background: var(--vio-color-surface-muted, #f2f2f2);
2099
+ position: relative;
2100
+ margin-inline: calc(
2101
+ var(--vio-detail-image-inset, var(--vio-detail-padding-x, var(--vio-space-lg, 24px))) -
2102
+ var(--vio-detail-padding-x, var(--vio-space-lg, 24px))
2103
+ );
2104
+ aspect-ratio: var(--vio-detail-image-aspect, 4 / 5);
2105
+ max-height: var(--vio-detail-image-max-height, 85vh);
2106
+ background: var(--vio-detail-image-bg, var(--vio-color-surface-muted, #f2f2f2));
2107
+ border-radius: var(--vio-detail-image-radius, 0);
1669
2108
  display: flex;
1670
2109
  align-items: center;
1671
2110
  justify-content: center;
1672
2111
  overflow: hidden;
1673
2112
  }
1674
2113
  .gallery-main img {
2114
+ display: block;
1675
2115
  width: 100%;
1676
2116
  height: 100%;
1677
- object-fit: contain;
1678
- padding: 24px;
1679
- box-sizing: border-box;
1680
- }
2117
+ max-height: var(--vio-detail-image-max-height, 85vh);
2118
+ object-fit: var(--vio-detail-image-fit, cover);
2119
+ object-position: var(--vio-detail-image-position, center);
2120
+ transform: scale(var(--vio-detail-image-zoom, 1));
2121
+ }
2122
+ /* Thumbnails: one row, never a second line — swipe on touch, arrows with a
2123
+ mouse; the selected one scrolls into view by itself. */
2124
+ .thumbs { position: relative; min-width: 0; }
1681
2125
  .gallery-thumbs {
1682
2126
  display: flex;
1683
- gap: 8px;
1684
- flex-wrap: wrap;
2127
+ flex-wrap: nowrap;
2128
+ gap: var(--vio-detail-thumb-gap, 8px);
2129
+ overflow-x: auto;
2130
+ scroll-snap-type: x proximity;
2131
+ scroll-behavior: smooth;
2132
+ scrollbar-width: none;
2133
+ -webkit-overflow-scrolling: touch;
1685
2134
  }
2135
+ .gallery-thumbs::-webkit-scrollbar { display: none; }
1686
2136
  .gallery-thumb {
1687
- width: 72px;
1688
- height: 72px;
2137
+ flex: 0 0 auto;
2138
+ width: var(--vio-detail-thumb-size, 64px);
2139
+ height: var(--vio-detail-thumb-size, 64px);
1689
2140
  background: var(--vio-color-surface-muted, #f2f2f2);
1690
- border: 1px solid transparent;
2141
+ border: var(--vio-detail-thumb-border-width, 1px) solid transparent;
2142
+ border-radius: var(--vio-detail-thumb-radius, 0);
1691
2143
  cursor: pointer;
1692
2144
  padding: 0;
1693
2145
  overflow: hidden;
2146
+ scroll-snap-align: start;
2147
+ box-sizing: border-box;
2148
+ opacity: var(--vio-detail-thumb-opacity, 1);
1694
2149
  }
1695
2150
  .gallery-thumb img {
2151
+ display: block;
1696
2152
  width: 100%;
1697
2153
  height: 100%;
1698
- object-fit: contain;
1699
- padding: 6px;
1700
- box-sizing: border-box;
2154
+ object-fit: var(--vio-detail-thumb-fit, cover);
2155
+ }
2156
+ .gallery-thumb.active {
2157
+ border-color: var(--vio-detail-thumb-active-color, var(--vio-color-text, #0a0a0a));
2158
+ opacity: 1;
2159
+ }
2160
+ .thumbs-arrow {
2161
+ display: none;
2162
+ position: absolute;
2163
+ top: 50%;
2164
+ transform: translateY(-50%);
2165
+ z-index: 1;
2166
+ width: 28px;
2167
+ height: 28px;
2168
+ border-radius: 50%;
2169
+ border: 1px solid var(--vio-color-border, #e5e5e5);
2170
+ background: var(--vio-color-surface, #fff);
2171
+ color: var(--vio-color-text, #0a0a0a);
2172
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.12);
2173
+ cursor: pointer;
2174
+ align-items: center;
2175
+ justify-content: center;
2176
+ padding: 0;
2177
+ font: 16px/1 sans-serif;
2178
+ }
2179
+ .thumbs-arrow.prev { left: -6px; }
2180
+ .thumbs-arrow.next { right: -6px; }
2181
+ .thumbs-arrow:disabled { visibility: hidden; }
2182
+ @media (hover: hover) and (pointer: fine) {
2183
+ .thumbs.overflows .thumbs-arrow { display: flex; }
1701
2184
  }
1702
- .gallery-thumb.active { border-color: var(--vio-color-text, #0a0a0a); }
1703
2185
 
1704
2186
  /* Info column */
1705
- .info { display: flex; flex-direction: column; gap: 24px; }
2187
+ .info { display: flex; flex-direction: column; gap: var(--vio-detail-info-gap, 20px); min-width: 0; }
1706
2188
  .brand {
1707
2189
  font-size: 12px;
1708
2190
  text-transform: uppercase;
@@ -1712,8 +2194,8 @@ var VioProductDetail = class extends LitElement {
1712
2194
  }
1713
2195
  .name {
1714
2196
  font-family: var(--vio-font-serif, Georgia, serif);
1715
- font-size: clamp(28px, 4vw, 40px);
1716
- font-weight: 400;
2197
+ font-size: var(--vio-detail-title-size, clamp(28px, 4vw, 40px));
2198
+ font-weight: var(--vio-detail-title-weight, 400);
1717
2199
  line-height: 1.15;
1718
2200
  margin: 0;
1719
2201
  letter-spacing: -0.01em;
@@ -1724,8 +2206,8 @@ var VioProductDetail = class extends LitElement {
1724
2206
  gap: 16px;
1725
2207
  }
1726
2208
  .price {
1727
- font-size: 24px;
1728
- font-weight: 700;
2209
+ font-size: var(--vio-detail-price-size, 24px);
2210
+ font-weight: var(--vio-detail-price-weight, 700);
1729
2211
  }
1730
2212
  .compare-at {
1731
2213
  font-size: 16px;
@@ -1743,7 +2225,7 @@ var VioProductDetail = class extends LitElement {
1743
2225
  .stock.out-of-stock { color: var(--vio-color-text-tertiary, #999); }
1744
2226
 
1745
2227
  /* Option pickers */
1746
- .option-group { display: flex; flex-direction: column; gap: 8px; }
2228
+ .option-group { display: flex; flex-direction: column; gap: 6px; }
1747
2229
  .option-label {
1748
2230
  font-size: 11px;
1749
2231
  text-transform: uppercase;
@@ -1751,14 +2233,28 @@ var VioProductDetail = class extends LitElement {
1751
2233
  color: var(--vio-color-text-secondary, #666);
1752
2234
  font-weight: 500;
1753
2235
  }
1754
- .option-values { display: flex; flex-wrap: wrap; gap: 8px; }
2236
+ /* Sizes: compact, still comfortable to tap. wrap → several lines;
2237
+ nowrap → one line that scrolls sideways. */
2238
+ .option-values {
2239
+ display: flex;
2240
+ flex-wrap: var(--vio-detail-option-wrap, wrap);
2241
+ gap: var(--vio-detail-option-gap, 6px);
2242
+ overflow-x: auto;
2243
+ scrollbar-width: none;
2244
+ }
2245
+ .option-values::-webkit-scrollbar { display: none; }
1755
2246
  .option-pill {
1756
- padding: 10px 18px;
2247
+ flex: 0 0 auto;
2248
+ box-sizing: border-box;
2249
+ min-height: var(--vio-detail-option-height, 36px);
2250
+ min-width: var(--vio-detail-option-height, 36px);
2251
+ padding: 0 var(--vio-detail-option-padding-x, 12px);
2252
+ border-radius: var(--vio-detail-option-radius, 0);
1757
2253
  background: #fff;
1758
2254
  border: 1px solid var(--vio-color-border, #e5e5e5);
1759
2255
  cursor: pointer;
1760
2256
  font-family: inherit;
1761
- font-size: 13px;
2257
+ font-size: var(--vio-detail-option-font-size, 13px);
1762
2258
  font-weight: 500;
1763
2259
  color: inherit;
1764
2260
  transition: border-color 0.15s, background 0.15s;
@@ -1777,6 +2273,34 @@ var VioProductDetail = class extends LitElement {
1777
2273
  }
1778
2274
  .option-pill.unavailable:hover { border-color: var(--vio-color-border, #e5e5e5); }
1779
2275
 
2276
+ /* The buy zone: Vipps first, then quantity + add to cart, then the other
2277
+ ways to pay. --vio-detail-buy-position: sticky pins it to the bottom of
2278
+ the panel while the shopper scrolls (mobile, mostly). */
2279
+ .buy {
2280
+ display: flex;
2281
+ flex-direction: column;
2282
+ gap: var(--vio-detail-buy-gap, 10px);
2283
+ position: var(--vio-detail-buy-position, static);
2284
+ bottom: 0;
2285
+ z-index: 2;
2286
+ background: var(--vio-detail-buy-bg, var(--vio-color-surface, #fff));
2287
+ padding: var(--vio-detail-buy-padding, 0);
2288
+ box-shadow: var(--vio-detail-buy-shadow, none);
2289
+ }
2290
+ .buy > * { margin-top: 0 !important; }
2291
+ /* Under the buttons: who the shopper is buying from. */
2292
+ .seller-note {
2293
+ margin: 2px 0 0;
2294
+ font-size: var(--vio-detail-seller-note-size, 12px);
2295
+ line-height: 1.4;
2296
+ color: var(--vio-color-text-secondary, #666);
2297
+ text-align: var(--vio-detail-seller-note-align, center);
2298
+ }
2299
+ .seller-note strong { font-weight: 600; color: var(--vio-color-text, #0a0a0a); }
2300
+ .seller-links { display: block; margin-top: 2px; }
2301
+ .seller-links a { color: inherit; text-decoration: underline; text-underline-offset: 2px; }
2302
+ .seller-links a + a::before { content: '·'; display: inline-block; margin: 0 6px; text-decoration: none; }
2303
+
1780
2304
  /* Qty + CTA row */
1781
2305
  .qty-cta-row {
1782
2306
  display: grid;
@@ -1791,7 +2315,7 @@ var VioProductDetail = class extends LitElement {
1791
2315
  }
1792
2316
  .qty-btn {
1793
2317
  width: 44px;
1794
- height: 56px;
2318
+ height: var(--vio-detail-button-height, 52px);
1795
2319
  background: #fff;
1796
2320
  border: none;
1797
2321
  cursor: pointer;
@@ -1809,7 +2333,9 @@ var VioProductDetail = class extends LitElement {
1809
2333
  background: var(--vio-color-text, #0a0a0a);
1810
2334
  color: var(--vio-color-text-on-primary, #fff);
1811
2335
  border: none;
1812
- padding: 18px 24px;
2336
+ min-height: var(--vio-detail-button-height, 52px);
2337
+ border-radius: var(--vio-detail-button-radius, 0);
2338
+ padding: 0 24px;
1813
2339
  font-size: 12px;
1814
2340
  text-transform: uppercase;
1815
2341
  letter-spacing: var(--vio-tracking-label, 0.1em);
@@ -1919,19 +2445,49 @@ var VioProductDetail = class extends LitElement {
1919
2445
  .buy-applepay:disabled { opacity: 0.5; cursor: not-allowed; }
1920
2446
 
1921
2447
  /* Description */
2448
+ /* The description folds: a tap opens it, so it does not take the page. */
2449
+ .description-block { border-top: 1px solid var(--vio-color-border, #e5e5e5); }
2450
+ .description-block > summary {
2451
+ list-style: none;
2452
+ cursor: pointer;
2453
+ display: flex;
2454
+ align-items: center;
2455
+ justify-content: space-between;
2456
+ padding: 14px 0;
2457
+ font-size: 11px;
2458
+ text-transform: uppercase;
2459
+ letter-spacing: var(--vio-tracking-label, 0.1em);
2460
+ color: var(--vio-color-text-secondary, #666);
2461
+ font-weight: 600;
2462
+ }
2463
+ .description-block > summary::-webkit-details-marker { display: none; }
2464
+ .description-block > summary::after {
2465
+ content: '';
2466
+ width: 8px;
2467
+ height: 8px;
2468
+ border-right: 1.5px solid currentColor;
2469
+ border-bottom: 1.5px solid currentColor;
2470
+ transform: rotate(45deg) translateY(-2px);
2471
+ transition: transform 0.15s ease;
2472
+ }
2473
+ .description-block[open] > summary::after { transform: rotate(-135deg) translateY(-2px); }
1922
2474
  .description {
1923
2475
  font-size: 14px;
1924
2476
  line-height: 1.6;
1925
2477
  color: var(--vio-color-text-secondary, #444);
1926
- margin-top: 8px;
2478
+ padding-bottom: 14px;
2479
+ white-space: pre-line;
1927
2480
  }
1928
- .description-label {
1929
- font-size: 11px;
1930
- text-transform: uppercase;
1931
- letter-spacing: var(--vio-tracking-label, 0.1em);
1932
- color: var(--vio-color-text-secondary, #666);
1933
- font-weight: 500;
1934
- margin-bottom: 8px;
2481
+ /* Desktop: the description above, the buy zone at the bottom of the panel
2482
+ (sticky). Phones keep the order as it is: buy, then the description. */
2483
+ @media (min-width: 601px) {
2484
+ .description-block { order: 1; }
2485
+ .buy {
2486
+ order: 2;
2487
+ position: var(--vio-detail-buy-position, sticky);
2488
+ padding: var(--vio-detail-buy-padding, 12px 0 16px);
2489
+ box-shadow: var(--vio-detail-buy-shadow, 0 -1px 0 var(--vio-color-border, #e5e5e5));
2490
+ }
1935
2491
  }
1936
2492
 
1937
2493
  /* Skeleton loader: shimmer placeholders mirroring the product layout
@@ -1987,14 +2543,47 @@ var VioProductDetail = class extends LitElement {
1987
2543
  disconnectedCallback() {
1988
2544
  document.removeEventListener("vio:product-click", this.boundProductClick);
1989
2545
  this.slotRegistration.clear();
2546
+ this.bottomSheet.disconnect();
1990
2547
  super.disconnectedCallback();
1991
2548
  }
1992
2549
  updated(changed) {
1993
- if (changed.has("open")) this.dialogFocus.update(this.open);
2550
+ if (changed.has("open")) {
2551
+ this.dialogFocus.update(this.open);
2552
+ this.bottomSheet.update(this.open);
2553
+ }
2554
+ if (changed.has("product") || changed.has("open")) this.measureThumbs();
2555
+ if (changed.has("activeImageIndex")) this.revealActiveThumb();
2556
+ this.measureLogo();
1994
2557
  if (changed.has("vioSlot") || changed.has("vioSlotName") || changed.has("vioScope") || changed.has("vioScopeName") || changed.has("vioCampaign")) {
1995
2558
  this.slotRegistration.sync(this.slotAttributes());
1996
2559
  }
1997
2560
  }
2561
+ thumbRow() {
2562
+ return this.renderRoot.querySelector(".gallery-thumbs");
2563
+ }
2564
+ /** Arrows only when the row overflows; each one off at its own end. */
2565
+ measureThumbs() {
2566
+ const row = this.thumbRow();
2567
+ if (!row) return;
2568
+ const overflow = row.scrollWidth > row.clientWidth + 1;
2569
+ if (overflow !== this.thumbsOverflow) this.thumbsOverflow = overflow;
2570
+ this.onThumbsScroll();
2571
+ }
2572
+ /** One arrow press moves most of a visible row. */
2573
+ scrollThumbs(direction) {
2574
+ const row = this.thumbRow();
2575
+ if (row) row.scrollBy({ left: direction * row.clientWidth * 0.8 });
2576
+ }
2577
+ /** The selected thumbnail is always visible: the row scrolls to it, the page never does. */
2578
+ revealActiveThumb() {
2579
+ const row = this.thumbRow();
2580
+ const thumb = row?.querySelectorAll(".gallery-thumb")[this.activeImageIndex];
2581
+ if (!row || !thumb) return;
2582
+ const left = thumb.offsetLeft - row.offsetLeft;
2583
+ if (left < row.scrollLeft || left + thumb.offsetWidth > row.scrollLeft + row.clientWidth) {
2584
+ row.scrollTo({ left: Math.max(0, left - (row.clientWidth - thumb.offsetWidth) / 2) });
2585
+ }
2586
+ }
1998
2587
  show() {
1999
2588
  this.open = true;
2000
2589
  }
@@ -2384,9 +2973,7 @@ var VioProductDetail = class extends LitElement {
2384
2973
  addCurrentToCart() {
2385
2974
  if (!this.product) return void 0;
2386
2975
  const variant = this.selectedVariant();
2387
- const sponsor = Vio.bootstrapCache?.primarySponsor;
2388
- const secondaries = Vio.bootstrapCache?.secondarySponsors ?? [];
2389
- const sponsorName = sponsor?.id === this.sponsorId ? sponsor.name : secondaries.find((s) => s.id === this.sponsorId)?.name;
2976
+ const sponsorName = resolveSponsorIdentity(this.sponsorId)?.name || void 0;
2390
2977
  const { campaignId, slot, contentId, componentTemplateId, contentUrl, contentTitle } = this.attribution();
2391
2978
  Vio.cart.addProduct({
2392
2979
  product: this.product,
@@ -2437,7 +3024,7 @@ var VioProductDetail = class extends LitElement {
2437
3024
  new CustomEvent("vio:checkout-open", {
2438
3025
  bubbles: true,
2439
3026
  composed: true,
2440
- detail: { sponsorId: this.sponsorId, paymentMethod: "klarna", express: true, ...this.attribution() }
3027
+ detail: { sponsorId: this.sponsorId, paymentMethod: "klarna", express: true, ...this.attribution(), ...sponsorDetail(this.sponsorId) }
2441
3028
  })
2442
3029
  );
2443
3030
  } finally {
@@ -2467,7 +3054,7 @@ var VioProductDetail = class extends LitElement {
2467
3054
  new CustomEvent("vio:checkout-open", {
2468
3055
  bubbles: true,
2469
3056
  composed: true,
2470
- detail: { sponsorId: this.sponsorId, express: false, ...this.attribution() }
3057
+ detail: { sponsorId: this.sponsorId, express: false, ...this.attribution(), ...sponsorDetail(this.sponsorId) }
2471
3058
  })
2472
3059
  );
2473
3060
  }
@@ -2483,27 +3070,33 @@ var VioProductDetail = class extends LitElement {
2483
3070
  }
2484
3071
  buyWithVipps() {
2485
3072
  if (!this.product || this.availableQuantity <= 0 || this.adding) return;
3073
+ const redirect = beginPaymentRedirect({ method: "vipps", sponsorId: this.sponsorId, source: "detail" });
2486
3074
  this.addCurrentToCart();
2487
3075
  this.close();
2488
- void this.startVippsExpressOrCheckout();
3076
+ void this.startVippsExpressOrCheckout(redirect);
2489
3077
  }
2490
- async startVippsExpressOrCheckout() {
3078
+ async startVippsExpressOrCheckout(redirect) {
2491
3079
  let fallback = true;
2492
3080
  try {
2493
- if (!await Vio.checkout.getVippsExpressEnabled(this.sponsorId)) throw new Error("express off");
2494
- const res = await Vio.checkout.startVippsExpress(this.sponsorId);
2495
- fallback = !res?.payment_url;
3081
+ if (!await Vio.checkout.getVippsExpressEnabled(this.sponsorId)) {
3082
+ redirect.abort("express-off");
3083
+ } else {
3084
+ const res = await Vio.checkout.startVippsExpress(this.sponsorId);
3085
+ fallback = !res?.payment_url;
3086
+ if (fallback) redirect.abort("not-started");
3087
+ }
2496
3088
  } catch (err) {
2497
3089
  if (typeof console !== "undefined") {
2498
3090
  console.warn("[VioProductDetail] Vipps Express not started:", err);
2499
3091
  }
3092
+ redirect.fail(err instanceof Error ? err.message : String(err));
2500
3093
  }
2501
3094
  if (!fallback) return;
2502
3095
  this.dispatchEvent(
2503
3096
  new CustomEvent("vio:checkout-open", {
2504
3097
  bubbles: true,
2505
3098
  composed: true,
2506
- detail: { sponsorId: this.sponsorId, paymentMethod: "vipps", express: false, ...this.attribution() }
3099
+ detail: { sponsorId: this.sponsorId, paymentMethod: "vipps", express: false, ...this.attribution(), ...sponsorDetail(this.sponsorId) }
2507
3100
  })
2508
3101
  );
2509
3102
  }
@@ -2516,7 +3109,7 @@ var VioProductDetail = class extends LitElement {
2516
3109
  new CustomEvent("vio:checkout-open", {
2517
3110
  bubbles: true,
2518
3111
  composed: true,
2519
- detail: { sponsorId: this.sponsorId, paymentMethod: "stripe", express: false, ...this.attribution() }
3112
+ detail: { sponsorId: this.sponsorId, paymentMethod: "stripe", express: false, ...this.attribution(), ...sponsorDetail(this.sponsorId) }
2520
3113
  })
2521
3114
  );
2522
3115
  }
@@ -2556,14 +3149,68 @@ var VioProductDetail = class extends LitElement {
2556
3149
  this.sellerName = name;
2557
3150
  this.sellerSettled = true;
2558
3151
  }
3152
+ /**
3153
+ * Who sells the product, top left beside the close (Mote & Livsstil,
3154
+ * 2026-10-22): the logo of the sponsor the product was fetched with — the
3155
+ * product's brand, never the article's — with its name as `alt`; without a
3156
+ * logo that loads, its name; without an identity, the seller's Company name
3157
+ * (or the product's brand) once known. Size: --vio-detail-logo-height.
3158
+ */
3159
+ renderTopbarSponsor() {
3160
+ const identity = resolveSponsorIdentity(this.sponsorId);
3161
+ const mark = identity ? renderSponsorMark(this, identity, { logo: "topbar-logo", name: "topbar-name" }) : nothing;
3162
+ if (mark !== nothing) {
3163
+ if (identity && usableLogo(identity)) {
3164
+ return html`<span class="topbar-logo-box">${mark}</span
3165
+ ><span class="topbar-name topbar-mark-name">${identity.name}</span>`;
3166
+ }
3167
+ return mark;
3168
+ }
3169
+ const name = this.headerName();
3170
+ return name ? html`<span class="topbar-name">${name}</span>` : nothing;
3171
+ }
3172
+ /**
3173
+ * Under the buy buttons: «Du handler direkte hos {brand}.» — the brand the
3174
+ * product was fetched with (the sponsor; its Company name or the product's
3175
+ * brand when no identity is known), never the article's. Nothing when no
3176
+ * name is known.
3177
+ */
3178
+ renderSellerNote() {
3179
+ const identity = resolveSponsorIdentity(this.sponsorId);
3180
+ const name = identity?.name || this.headerName();
3181
+ if (!name) return nothing;
3182
+ const links = [
3183
+ identity?.termsUrl ? html`<a href=${identity.termsUrl} target="_blank" rel="noopener noreferrer" part="seller-terms">Kjøpsvilkår</a>` : null,
3184
+ identity?.refundPolicyUrl ? html`<a href=${identity.refundPolicyUrl} target="_blank" rel="noopener noreferrer" part="seller-refund">Angrerett og retur</a>` : null
3185
+ ].filter(Boolean);
3186
+ return html`<p class="seller-note" part="seller-note">
3187
+ Du handler direkte hos <strong>${name}</strong>.${links.length ? html`<span class="seller-links">${links}</span>` : nothing}
3188
+ </p>`;
3189
+ }
3190
+ /** Square-ish logo (4:5 to 5:4) → the top bar shows the name beside it. */
3191
+ measureLogo() {
3192
+ const img = this.renderRoot.querySelector(".topbar-logo");
3193
+ if (!img) {
3194
+ if (this.logoSquare) this.logoSquare = false;
3195
+ return;
3196
+ }
3197
+ const decide = () => {
3198
+ if (!img.naturalWidth || !img.naturalHeight) return;
3199
+ const ratio = img.naturalWidth / img.naturalHeight;
3200
+ const square = ratio >= 0.8 && ratio <= 1.25;
3201
+ if (square !== this.logoSquare) this.logoSquare = square;
3202
+ };
3203
+ if (img.complete) decide();
3204
+ else img.addEventListener("load", decide, { once: true });
3205
+ }
2559
3206
  render() {
2560
3207
  return html`
2561
3208
  <div class="backdrop" @click=${this.close}></div>
2562
3209
  <div class="modal" role="dialog" aria-modal="true" aria-label="Produkt" tabindex="-1">
2563
3210
  <div class="handle" @click=${this.close} aria-hidden="true"></div>
2564
- <div class="topbar">
2565
- <span class="topbar-brand">${this.headerName()}</span>
2566
- <button class="close" @click=${this.close} aria-label="Lukk">×</button>
3211
+ <div class="topbar" part="topbar">
3212
+ <div class="topbar-sponsor ${this.logoSquare ? "is-square" : ""}" part="sponsor">${this.renderTopbarSponsor()}</div>
3213
+ <button class="close" part="close" @click=${this.close} aria-label="Lukk">×</button>
2567
3214
  </div>
2568
3215
 
2569
3216
  ${this.renderBody()}
@@ -2595,44 +3242,90 @@ var VioProductDetail = class extends LitElement {
2595
3242
  const activeImage = this.activeImage;
2596
3243
  return html`
2597
3244
  <div class="body">
2598
- <div class="gallery">
2599
- <div class="gallery-main">
3245
+ <div class="gallery" part="gallery">
3246
+ <div class="gallery-main" part="image">
2600
3247
  ${activeImage?.url ? html`<img src=${activeImage.url} alt=${p.title} />` : ""}
2601
3248
  </div>
2602
3249
  ${images.length > 1 ? html`
2603
- <div class="gallery-thumbs">
2604
- ${images.map(
3250
+ <div class="thumbs ${this.thumbsOverflow ? "overflows" : ""}">
3251
+ <button
3252
+ class="thumbs-arrow prev"
3253
+ part="thumbs-arrow"
3254
+ aria-label="Forrige bilder"
3255
+ ?disabled=${this.thumbsAtStart}
3256
+ @click=${() => this.scrollThumbs(-1)}
3257
+ >‹</button>
3258
+ <div class="gallery-thumbs" part="thumbs" @scroll=${this.onThumbsScroll}>
3259
+ ${images.map(
2605
3260
  (img, idx) => html`
2606
- <button
2607
- class="gallery-thumb ${idx === this.activeImageIndex ? "active" : ""}"
2608
- @click=${() => this.activeImageIndex = idx}
2609
- aria-label=${`Bilde ${idx + 1}`}
2610
- >
2611
- <img src=${img.url} alt="" />
2612
- </button>
2613
- `
3261
+ <button
3262
+ class="gallery-thumb ${idx === this.activeImageIndex ? "active" : ""}"
3263
+ part=${idx === this.activeImageIndex ? "thumb thumb-active" : "thumb"}
3264
+ @click=${() => this.activeImageIndex = idx}
3265
+ aria-label=${`Bilde ${idx + 1}`}
3266
+ aria-current=${idx === this.activeImageIndex ? "true" : "false"}
3267
+ >
3268
+ <img src=${img.url} alt="" loading="lazy" />
3269
+ </button>
3270
+ `
2614
3271
  )}
3272
+ </div>
3273
+ <button
3274
+ class="thumbs-arrow next"
3275
+ part="thumbs-arrow"
3276
+ aria-label="Neste bilder"
3277
+ ?disabled=${this.thumbsAtEnd}
3278
+ @click=${() => this.scrollThumbs(1)}
3279
+ >›</button>
2615
3280
  </div>
2616
3281
  ` : ""}
2617
3282
  </div>
2618
3283
 
2619
- <div class="info">
2620
- ${p.brand ? html`<div class="brand">${p.brand}</div>` : ""}
2621
- <h2 class="name">${p.title}</h2>
3284
+ <div class="info" part="info">
3285
+ ${p.brand ? html`<div class="brand" part="brand">${p.brand}</div>` : ""}
3286
+ <h2 class="name" part="title">${p.title}</h2>
2622
3287
 
2623
3288
  <div class="price-row">
2624
- <span class="price">${formatPrice(this.unitPrice, p.price.currency_code)}</span>
3289
+ <span class="price" part="price">${formatPrice(this.unitPrice, p.price.currency_code)}</span>
2625
3290
  ${showCompare ? html`<span class="compare-at">${formatPrice(
2626
3291
  compare,
2627
3292
  p.price.currency_code
2628
3293
  )}</span>` : ""}
2629
3294
  </div>
2630
3295
 
2631
- <div class="stock ${stock.cls}">${stock.text}</div>
3296
+ <div class="stock ${stock.cls}" part="stock">${stock.text}</div>
2632
3297
 
2633
3298
  ${this.renderOptionPickers(p.options)}
2634
3299
 
2635
- <div class="qty-cta-row">
3300
+ <div class="buy" part="buy">
3301
+ ${this.availableQuantity > 0 && this.methodEnabled("vipps") ? this.vippsOfficial ? html`
3302
+ <vipps-mobilepay-button
3303
+ class="buy-vipps-official"
3304
+ part="buy-vipps"
3305
+ brand="vipps"
3306
+ language="no"
3307
+ verb="buy"
3308
+ variant="primary"
3309
+ rounded="true"
3310
+ stretched="true"
3311
+ role="button"
3312
+ aria-label="Kjøp nå med Vipps"
3313
+ @click=${this.buyWithVipps}
3314
+ ></vipps-mobilepay-button>
3315
+ ` : html`
3316
+ <button
3317
+ class="buy-vipps"
3318
+ part="buy-vipps"
3319
+ @click=${this.buyWithVipps}
3320
+ ?disabled=${this.adding}
3321
+ aria-label="Kjøp nå med Vipps"
3322
+ >
3323
+ <span>Kjøp nå med</span>
3324
+ <span class="vipps-badge">vipps</span>
3325
+ </button>
3326
+ ` : ""}
3327
+
3328
+ <div class="qty-cta-row" part="quantity-row">
2636
3329
  <div class="qty-stepper">
2637
3330
  <button
2638
3331
  class="qty-btn"
@@ -2653,6 +3346,7 @@ var VioProductDetail = class extends LitElement {
2653
3346
  </div>
2654
3347
  <button
2655
3348
  class="add-to-cart"
3349
+ part="add-to-cart"
2656
3350
  @click=${this.addToCart}
2657
3351
  ?disabled=${this.availableQuantity <= 0 || this.adding}
2658
3352
  >
@@ -2714,36 +3408,14 @@ var VioProductDetail = class extends LitElement {
2714
3408
  </button>
2715
3409
  ` : ""}
2716
3410
 
2717
- ${this.availableQuantity > 0 && this.methodEnabled("vipps") ? this.vippsOfficial ? html`
2718
- <vipps-mobilepay-button
2719
- class="buy-vipps-official"
2720
- brand="vipps"
2721
- language="no"
2722
- verb="buy"
2723
- variant="primary"
2724
- rounded="true"
2725
- stretched="true"
2726
- role="button"
2727
- aria-label="Kjøp nå med Vipps"
2728
- @click=${this.buyWithVipps}
2729
- ></vipps-mobilepay-button>
2730
- ` : html`
2731
- <button
2732
- class="buy-vipps"
2733
- @click=${this.buyWithVipps}
2734
- ?disabled=${this.adding}
2735
- aria-label="Kjøp nå med Vipps"
2736
- >
2737
- <span>Kjøp nå med</span>
2738
- <span class="vipps-badge">vipps</span>
2739
- </button>
2740
- ` : ""}
3411
+ ${this.renderSellerNote()}
3412
+ </div>
2741
3413
 
2742
3414
  ${p.description ? html`
2743
- <div>
2744
- <div class="description-label">Beskrivelse</div>
3415
+ <details class="description-block" part="description">
3416
+ <summary>Beskrivelse</summary>
2745
3417
  <div class="description">${this.stripHtml(p.description)}</div>
2746
- </div>
3418
+ </details>
2747
3419
  ` : ""}
2748
3420
  </div>
2749
3421
  </div>
@@ -2755,9 +3427,9 @@ var VioProductDetail = class extends LitElement {
2755
3427
  const values = normalizeValues(opt.values);
2756
3428
  if (values.length === 0) return "";
2757
3429
  return html`
2758
- <div class="option-group">
2759
- <div class="option-label">${opt.name}</div>
2760
- <div class="option-values">
3430
+ <div class="option-group" part="option-group">
3431
+ <div class="option-label" part="option-label">${opt.name}</div>
3432
+ <div class="option-values" part="options">
2761
3433
  ${values.map((val) => {
2762
3434
  const available = this.isValueAvailable(opt.name, val);
2763
3435
  const currentVal = this.selectedOptions[opt.name] ?? "";
@@ -2765,6 +3437,7 @@ var VioProductDetail = class extends LitElement {
2765
3437
  return html`
2766
3438
  <button
2767
3439
  class="option-pill ${available ? "" : "unavailable"}"
3440
+ part=${isSelected ? "option option-selected" : "option"}
2768
3441
  @click=${() => this.selectOption(opt.name, val)}
2769
3442
  aria-pressed=${isSelected}
2770
3443
  ?disabled=${!available}
@@ -2780,8 +3453,8 @@ var VioProductDetail = class extends LitElement {
2780
3453
  })}`;
2781
3454
  }
2782
3455
  /** Best-effort HTML strip for description. */
2783
- stripHtml(html6) {
2784
- return html6.replace(/<br\s*\/?>/gi, "\n").replace(/<\/p>/gi, "\n\n").replace(/<[^>]+>/g, "").replace(/&nbsp;/g, " ").replace(/&amp;/g, "&").replace(/&lt;/g, "<").replace(/&gt;/g, ">").trim();
3456
+ stripHtml(html7) {
3457
+ return html7.replace(/<br\s*\/?>/gi, "\n").replace(/<\/p>/gi, "\n\n").replace(/<[^>]+>/g, "").replace(/&nbsp;/g, " ").replace(/&amp;/g, "&").replace(/&lt;/g, "<").replace(/&gt;/g, ">").trim();
2785
3458
  }
2786
3459
  };
2787
3460
  __decorateClass([
@@ -2844,6 +3517,18 @@ __decorateClass([
2844
3517
  __decorateClass([
2845
3518
  state()
2846
3519
  ], VioProductDetail.prototype, "activeImageIndex", 2);
3520
+ __decorateClass([
3521
+ state()
3522
+ ], VioProductDetail.prototype, "thumbsOverflow", 2);
3523
+ __decorateClass([
3524
+ state()
3525
+ ], VioProductDetail.prototype, "thumbsAtStart", 2);
3526
+ __decorateClass([
3527
+ state()
3528
+ ], VioProductDetail.prototype, "thumbsAtEnd", 2);
3529
+ __decorateClass([
3530
+ state()
3531
+ ], VioProductDetail.prototype, "logoSquare", 2);
2847
3532
  __decorateClass([
2848
3533
  state()
2849
3534
  ], VioProductDetail.prototype, "adding", 2);
@@ -2895,6 +3580,14 @@ var VioCart = class extends LitElement {
2895
3580
  container: () => this.renderRoot.querySelector(".drawer"),
2896
3581
  close: () => this.close()
2897
3582
  });
3583
+ /** Phones: the page behind is locked, and the sheet closes by pulling the bar or the header down. */
3584
+ this.bottomSheet = new BottomSheet({
3585
+ host: this,
3586
+ sheet: () => this.renderRoot.querySelector(".drawer"),
3587
+ backdrop: () => this.renderRoot.querySelector(".backdrop"),
3588
+ dragZones: ".handle, .header",
3589
+ close: () => this.close()
3590
+ });
2898
3591
  this.boundOnCartChange = (e) => {
2899
3592
  const detail = e.detail;
2900
3593
  this.carts = detail.cartsBySponsor;
@@ -2925,7 +3618,7 @@ var VioCart = class extends LitElement {
2925
3618
  top: 0;
2926
3619
  right: 0;
2927
3620
  bottom: 0;
2928
- width: min(440px, 100%);
3621
+ width: min(var(--vio-cart-width, 440px), 100%);
2929
3622
  background: var(--vio-color-surface, #fff);
2930
3623
  transform: translateX(100%);
2931
3624
  transition: transform 0.3s cubic-bezier(0.32, 0.72, 0, 1);
@@ -2954,8 +3647,11 @@ var VioCart = class extends LitElement {
2954
3647
  border-radius: var(--vio-radius-xl, 16px) var(--vio-radius-xl, 16px) 0 0;
2955
3648
  transform: translateY(100%);
2956
3649
  box-shadow: 0 -8px 24px rgba(0, 0, 0, 0.08);
3650
+ overscroll-behavior: contain;
2957
3651
  }
2958
3652
  :host([open]) .drawer { transform: translateY(0); }
3653
+ /* Pulled down to close (bottom-sheet.ts): the browser must not take the gesture for a scroll. */
3654
+ .handle, .header { touch-action: none; }
2959
3655
  .handle {
2960
3656
  display: flex;
2961
3657
  justify-content: center;
@@ -3275,6 +3971,7 @@ var VioCart = class extends LitElement {
3275
3971
  }
3276
3972
  disconnectedCallback() {
3277
3973
  Vio.cart.removeEventListener("change", this.boundOnCartChange);
3974
+ this.bottomSheet.disconnect();
3278
3975
  super.disconnectedCallback();
3279
3976
  }
3280
3977
  close() {
@@ -3321,7 +4018,10 @@ var VioCart = class extends LitElement {
3321
4018
  return isMethodEnabled(this.availableMethods, ...names);
3322
4019
  }
3323
4020
  updated(changed) {
3324
- if (changed.has("open")) this.dialogFocus.update(this.open);
4021
+ if (changed.has("open")) {
4022
+ this.dialogFocus.update(this.open);
4023
+ this.bottomSheet.update(this.open);
4024
+ }
3325
4025
  if (changed.has("open") && this.open) {
3326
4026
  this.confirmedOrder = null;
3327
4027
  this.preparedTotal = -1;
@@ -3356,6 +4056,8 @@ var VioCart = class extends LitElement {
3356
4056
  const handle = this.applePayHandle;
3357
4057
  if (!handle || !handle.available || this.itemCount === 0) return;
3358
4058
  const snapshot = this.buildConfirmation();
4059
+ const sponsorIds = [...this.carts.keys()];
4060
+ const sponsor = sponsorIds.length === 1 ? sponsorDetail(sponsorIds[0]) : {};
3359
4061
  void handle.show().then((result) => {
3360
4062
  if (!result) return;
3361
4063
  this.confirmedOrder = snapshot;
@@ -3367,7 +4069,7 @@ var VioCart = class extends LitElement {
3367
4069
  new CustomEvent("vio:payment-success", {
3368
4070
  bubbles: true,
3369
4071
  composed: true,
3370
- detail: { method: "apple-pay", result, ...buyerRef ? { buyerRef } : {} }
4072
+ detail: { method: "apple-pay", result, ...buyerRef ? { buyerRef } : {}, ...sponsor }
3371
4073
  })
3372
4074
  );
3373
4075
  });
@@ -3403,7 +4105,7 @@ var VioCart = class extends LitElement {
3403
4105
  new CustomEvent("vio:checkout-open", {
3404
4106
  bubbles: true,
3405
4107
  composed: true,
3406
- detail: { sponsorId: firstSponsorId, paymentMethod: "klarna", express: true }
4108
+ detail: { sponsorId: firstSponsorId, paymentMethod: "klarna", express: true, ...sponsorDetail(firstSponsorId) }
3407
4109
  })
3408
4110
  );
3409
4111
  }
@@ -3424,7 +4126,7 @@ var VioCart = class extends LitElement {
3424
4126
  new CustomEvent("vio:checkout-open", {
3425
4127
  bubbles: true,
3426
4128
  composed: true,
3427
- detail: { sponsorId: firstSponsorId, express: false }
4129
+ detail: { sponsorId: firstSponsorId, express: false, ...sponsorDetail(firstSponsorId) }
3428
4130
  })
3429
4131
  );
3430
4132
  }
@@ -3438,30 +4140,36 @@ var VioCart = class extends LitElement {
3438
4140
  if (this.itemCount === 0) return;
3439
4141
  const firstSponsorId = [...this.carts.keys()][0];
3440
4142
  if (firstSponsorId === void 0) return;
3441
- void this.startVippsExpressOrCheckout(firstSponsorId);
4143
+ const redirect = beginPaymentRedirect({ method: "vipps", sponsorId: firstSponsorId, source: "cart" });
4144
+ void this.startVippsExpressOrCheckout(firstSponsorId, redirect);
3442
4145
  }
3443
4146
  /** Vipps' own button when its script answers; our orange one otherwise. */
3444
4147
  async loadVippsButton() {
3445
4148
  this.vippsOfficial = await ensureVippsButton();
3446
4149
  }
3447
- async startVippsExpressOrCheckout(sponsorId) {
4150
+ async startVippsExpressOrCheckout(sponsorId, redirect) {
3448
4151
  let fallback = true;
3449
4152
  try {
3450
4153
  await Vio.cart.waitForCartMutations(sponsorId);
3451
- if (!await Vio.checkout.getVippsExpressEnabled(sponsorId)) throw new Error("express off");
3452
- const res = await Vio.checkout.startVippsExpress(sponsorId);
3453
- fallback = !res?.payment_url;
4154
+ if (!await Vio.checkout.getVippsExpressEnabled(sponsorId)) {
4155
+ redirect.abort("express-off");
4156
+ } else {
4157
+ const res = await Vio.checkout.startVippsExpress(sponsorId);
4158
+ fallback = !res?.payment_url;
4159
+ if (fallback) redirect.abort("not-started");
4160
+ }
3454
4161
  } catch (err) {
3455
4162
  if (typeof console !== "undefined") {
3456
4163
  console.warn("[VioCart] Vipps Express not started:", err);
3457
4164
  }
4165
+ redirect.fail(err instanceof Error ? err.message : String(err));
3458
4166
  }
3459
4167
  if (!fallback) return;
3460
4168
  this.dispatchEvent(
3461
4169
  new CustomEvent("vio:checkout-open", {
3462
4170
  bubbles: true,
3463
4171
  composed: true,
3464
- detail: { sponsorId, paymentMethod: "vipps", express: false }
4172
+ detail: { sponsorId, paymentMethod: "vipps", express: false, ...sponsorDetail(sponsorId) }
3465
4173
  })
3466
4174
  );
3467
4175
  }
@@ -3474,10 +4182,14 @@ var VioCart = class extends LitElement {
3474
4182
  new CustomEvent("vio:checkout-open", {
3475
4183
  bubbles: true,
3476
4184
  composed: true,
3477
- detail: { sponsorId: firstSponsorId, paymentMethod: "stripe", express: false }
4185
+ detail: { sponsorId: firstSponsorId, paymentMethod: "stripe", express: false, ...sponsorDetail(firstSponsorId) }
3478
4186
  })
3479
4187
  );
3480
4188
  }
4189
+ /** The name over a sponsor's lines: the brand's display name by the one rule, else what the cart kept. */
4190
+ sponsorLabel(cart) {
4191
+ return resolveSponsorIdentity(cart.sponsorId)?.name || cart.sponsorName || "";
4192
+ }
3481
4193
  get totalAcrossSponsors() {
3482
4194
  let sum = 0;
3483
4195
  for (const cart of this.carts.values()) {
@@ -3518,7 +4230,7 @@ var VioCart = class extends LitElement {
3518
4230
  ${sponsorCarts.map(
3519
4231
  (cart) => html`
3520
4232
  <div class="sponsor-group">
3521
- ${cart.sponsorName ? html`<div class="sponsor-label">${cart.sponsorName}</div>` : ""}
4233
+ ${this.sponsorLabel(cart) ? html`<div class="sponsor-label">${this.sponsorLabel(cart)}</div>` : ""}
3522
4234
  ${cart.items.map(
3523
4235
  (item) => html`
3524
4236
  <div class="item">
@@ -3755,6 +4467,9 @@ var _VioCheckout = class _VioCheckout extends LitElement {
3755
4467
  this.verifyingReturn = null;
3756
4468
  /** The checkout whose confirmation is on screen — see fillOrderNumber. */
3757
4469
  this.confirmedCheckoutId = null;
4470
+ this.orderSponsor = null;
4471
+ /** The bootstrap that names the checkout's sponsor was asked for (once per checkout) — see watchSponsorBootstrap. */
4472
+ this.sponsorBootstrapAsked = false;
3758
4473
  this.confirmedOrder = null;
3759
4474
  /** Active Klarna Payments widget handle + the subtotal it was mounted for. */
3760
4475
  this.klarnaHandle = null;
@@ -3853,6 +4568,15 @@ var _VioCheckout = class _VioCheckout extends LitElement {
3853
4568
  close: () => this.close(),
3854
4569
  canClose: () => !this.paymentInFlight
3855
4570
  });
4571
+ /** Phones: the page behind is locked, and the sheet closes by pulling the bar or the top bar down — not mid-payment. */
4572
+ this.bottomSheet = new BottomSheet({
4573
+ host: this,
4574
+ sheet: () => this.renderRoot.querySelector(".modal"),
4575
+ backdrop: () => this.renderRoot.querySelector(".backdrop"),
4576
+ dragZones: ".handle, .topbar",
4577
+ close: () => this.close(),
4578
+ canClose: () => !this.paymentInFlight
4579
+ });
3856
4580
  this.paymentMethodsResolved = false;
3857
4581
  /** Guards `autoSelectSoleMethod` to ONE attempt per opening — see there. */
3858
4582
  this.autoSelectAttempted = false;
@@ -3905,6 +4629,8 @@ var _VioCheckout = class _VioCheckout extends LitElement {
3905
4629
  this.paymentNotice = null;
3906
4630
  this.verifyingReturn = null;
3907
4631
  this.confirmedCheckoutId = null;
4632
+ this.orderSponsor = null;
4633
+ this.sponsorBootstrapAsked = false;
3908
4634
  this.express = false;
3909
4635
  this.availableMethods = null;
3910
4636
  this.paymentMethodsResolved = false;
@@ -4003,8 +4729,8 @@ var _VioCheckout = class _VioCheckout extends LitElement {
4003
4729
  from the right instead of the full-screen overlay. */
4004
4730
  .modal.as-side {
4005
4731
  inset: 0 0 0 auto;
4006
- width: min(440px, 100%);
4007
- max-width: 440px;
4732
+ width: min(var(--vio-checkout-width, 440px), 100%);
4733
+ max-width: var(--vio-checkout-width, 440px);
4008
4734
  transform: translateX(100%);
4009
4735
  border-radius: 0;
4010
4736
  box-shadow: -8px 0 24px rgba(0, 0, 0, 0.1);
@@ -4019,8 +4745,8 @@ var _VioCheckout = class _VioCheckout extends LitElement {
4019
4745
  @media (min-width: 601px) {
4020
4746
  .modal {
4021
4747
  inset: 0 0 0 auto;
4022
- width: min(440px, 100%);
4023
- max-width: 440px;
4748
+ width: min(var(--vio-checkout-width, 440px), 100%);
4749
+ max-width: var(--vio-checkout-width, 440px);
4024
4750
  transform: translateX(100%);
4025
4751
  border-radius: 0;
4026
4752
  box-shadow: -8px 0 24px rgba(0, 0, 0, 0.1);
@@ -4057,7 +4783,10 @@ var _VioCheckout = class _VioCheckout extends LitElement {
4057
4783
  height: 92vh;
4058
4784
  border-radius: var(--vio-radius-xl, 16px) var(--vio-radius-xl, 16px) 0 0;
4059
4785
  box-shadow: 0 -8px 24px rgba(0, 0, 0, 0.08);
4786
+ overscroll-behavior: contain;
4060
4787
  }
4788
+ /* Pulled down to close (bottom-sheet.ts): the browser must not take the gesture for a scroll. */
4789
+ .handle, .topbar { touch-action: none; }
4061
4790
  .handle {
4062
4791
  display: flex;
4063
4792
  justify-content: center;
@@ -4090,6 +4819,33 @@ var _VioCheckout = class _VioCheckout extends LitElement {
4090
4819
  font-weight: 400;
4091
4820
  margin: 0;
4092
4821
  }
4822
+ .topbar-title {
4823
+ display: flex;
4824
+ flex-direction: column;
4825
+ gap: 4px;
4826
+ min-width: 0;
4827
+ }
4828
+ /* Who the shopper buys from — the checkout's sponsor, compact under the heading. */
4829
+ .topbar-sponsor {
4830
+ margin: 0;
4831
+ display: flex;
4832
+ align-items: center;
4833
+ gap: 6px;
4834
+ font-size: 12px;
4835
+ line-height: 1.3;
4836
+ color: var(--vio-color-text-secondary, #666);
4837
+ }
4838
+ .topbar-sponsor-logo {
4839
+ display: block;
4840
+ width: auto;
4841
+ height: 18px;
4842
+ max-width: 120px;
4843
+ object-fit: contain;
4844
+ }
4845
+ .topbar-sponsor-name {
4846
+ color: var(--vio-color-text, #0a0a0a);
4847
+ font-weight: 600;
4848
+ }
4093
4849
  .close {
4094
4850
  background: none;
4095
4851
  border: none;
@@ -4405,7 +5161,7 @@ var _VioCheckout = class _VioCheckout extends LitElement {
4405
5161
  width: 48px;
4406
5162
  height: 48px;
4407
5163
  border-radius: 50%;
4408
- background: #2e7d32;
5164
+ background: var(--vio-confirm-check-bg, #2e7d32);
4409
5165
  color: #fff;
4410
5166
  display: flex;
4411
5167
  align-items: center;
@@ -4415,10 +5171,26 @@ var _VioCheckout = class _VioCheckout extends LitElement {
4415
5171
  }
4416
5172
  .confirm-title {
4417
5173
  font-family: var(--vio-font-serif, Georgia, serif);
4418
- font-size: 24px;
4419
- font-weight: 400;
5174
+ font-size: var(--vio-confirm-title-size, 24px);
5175
+ font-weight: var(--vio-confirm-title-weight, 400);
4420
5176
  margin: 0;
4421
5177
  }
5178
+ /* The brand the order is with: its logo over the thanks, its name in it. */
5179
+ .confirm-brand { display: flex; align-items: center; }
5180
+ .confirm-brand-logo {
5181
+ display: block;
5182
+ width: auto;
5183
+ height: var(--vio-confirm-logo-height, 40px);
5184
+ max-width: var(--vio-confirm-logo-max-width, 220px);
5185
+ object-fit: contain;
5186
+ }
5187
+ .confirm-sponsor {
5188
+ font-size: var(--vio-confirm-text-size, 14px);
5189
+ line-height: 1.4;
5190
+ color: var(--vio-color-text, #0a0a0a);
5191
+ margin: 4px 0 0;
5192
+ }
5193
+ .confirm-sponsor strong { font-weight: 600; }
4422
5194
  .confirm-text {
4423
5195
  font-size: 14px;
4424
5196
  color: var(--vio-color-text-secondary, #666);
@@ -4619,6 +5391,7 @@ var _VioCheckout = class _VioCheckout extends LitElement {
4619
5391
  * Vipps-specific (GetVippsStatus) return-verification paths. */
4620
5392
  applyReturnOutcome(outcome, sawStatus, vioMethod, sponsorId, checkoutId) {
4621
5393
  if (outcome === "paid") {
5394
+ this.setOrderSponsor(sponsorId, checkoutId);
4622
5395
  if (sponsorId > 0) Vio.cart.clearSponsorCart(sponsorId);
4623
5396
  this.confirmedOrder = {
4624
5397
  items: [],
@@ -4636,6 +5409,7 @@ var _VioCheckout = class _VioCheckout extends LitElement {
4636
5409
  } else if (outcome === "failed") {
4637
5410
  this.paymentError = "Betalingen ble avbrutt eller feilet. Vennligst pr\xF8v igjen.";
4638
5411
  this.open = true;
5412
+ returnCancelled({ method: vioMethod, sponsorId, source: "checkout", checkoutId });
4639
5413
  } else if (sawStatus) {
4640
5414
  this.paymentNotice = "Betalingen behandles fortsatt. Du f\xE5r ordrebekreftelse p\xE5 e-post n\xE5r den er gjennomf\xF8rt. Handlekurven er uendret.";
4641
5415
  this.open = true;
@@ -4679,6 +5453,7 @@ var _VioCheckout = class _VioCheckout extends LitElement {
4679
5453
  if (kustomReturn?.kind === "confirmation") {
4680
5454
  const kustomOrderId = kustomReturn.orderId;
4681
5455
  this.returningFrom = "kustom";
5456
+ this.setOrderSponsor(sponsorId, kustomReturn.checkoutId);
4682
5457
  if (!this.checkoutState) {
4683
5458
  const spIdToUse = sponsorId || [...Vio.cart.getAllCarts().keys()][0] || 0;
4684
5459
  if (spIdToUse) {
@@ -4722,6 +5497,7 @@ var _VioCheckout = class _VioCheckout extends LitElement {
4722
5497
  if (!vioPayment && !checkoutId) return;
4723
5498
  if (urlParams.get("payment_processor") === "QLIRO" && checkoutId) {
4724
5499
  this.returningFrom = "qliro";
5500
+ this.setOrderSponsor(sponsorId, checkoutId);
4725
5501
  if (!this.checkoutState) {
4726
5502
  const spIdToUse = sponsorId || [...Vio.cart.getAllCarts().keys()][0] || 0;
4727
5503
  if (spIdToUse) {
@@ -4776,6 +5552,7 @@ var _VioCheckout = class _VioCheckout extends LitElement {
4776
5552
  }
4777
5553
  if (urlParams.get("payment_processor") === "WALLEY" && checkoutId) {
4778
5554
  this.returningFrom = "walley";
5555
+ this.setOrderSponsor(sponsorId, checkoutId);
4779
5556
  if (!this.checkoutState) {
4780
5557
  const spIdToUse = sponsorId || [...Vio.cart.getAllCarts().keys()][0] || 0;
4781
5558
  if (spIdToUse) {
@@ -4832,6 +5609,7 @@ var _VioCheckout = class _VioCheckout extends LitElement {
4832
5609
  this.cleanReturnQueryParams(ADYEN_RETURN_QUERY_KEYS);
4833
5610
  if (!adyenReturn) return;
4834
5611
  this.returningFrom = "adyen";
5612
+ this.setOrderSponsor(sponsorId, checkoutId);
4835
5613
  this.adyenSettled = true;
4836
5614
  this.autoSelectAttempted = true;
4837
5615
  if (!this.checkoutState) {
@@ -4888,6 +5666,7 @@ var _VioCheckout = class _VioCheckout extends LitElement {
4888
5666
  }
4889
5667
  }
4890
5668
  }
5669
+ this.setOrderSponsor(sponsorId, checkoutId);
4891
5670
  this.open = true;
4892
5671
  this.verifyingReturn = "Bekrefter betalingen med Vipps\u2026";
4893
5672
  const { outcome, sawStatus } = await this.pollVippsStatus(checkoutId, sponsorId);
@@ -4898,6 +5677,7 @@ var _VioCheckout = class _VioCheckout extends LitElement {
4898
5677
  let outcome = "pending";
4899
5678
  let sawStatus = false;
4900
5679
  if (checkoutId) {
5680
+ this.setOrderSponsor(sponsorId, checkoutId);
4901
5681
  this.open = true;
4902
5682
  this.verifyingReturn = "Bekrefter betalingen\u2026";
4903
5683
  for (const delayMs of _VioCheckout.RETURN_VERIFY_DELAYS_MS) {
@@ -4972,10 +5752,14 @@ var _VioCheckout = class _VioCheckout extends LitElement {
4972
5752
  this.unmountNexi();
4973
5753
  this.unmountAdyen();
4974
5754
  for (const settle of [...this.pendingSuccessEmits]) settle();
5755
+ this.bottomSheet.disconnect();
4975
5756
  super.disconnectedCallback();
4976
5757
  }
4977
5758
  updated(changed) {
4978
- if (changed.has("open")) this.dialogFocus.update(this.open);
5759
+ if (changed.has("open")) {
5760
+ this.dialogFocus.update(this.open);
5761
+ this.bottomSheet.update(this.open);
5762
+ }
4979
5763
  if (changed.has("open") && this.open && this.items.length > 0 && this.availableShippingsList.length === 0) {
4980
5764
  void this.loadAvailableShippings();
4981
5765
  }
@@ -4996,6 +5780,7 @@ var _VioCheckout = class _VioCheckout extends LitElement {
4996
5780
  this.unmountStripe();
4997
5781
  this.paymentError = null;
4998
5782
  }
5783
+ this.watchSponsorBootstrap();
4999
5784
  if (!this.isConnected) return;
5000
5785
  void this.mountKlarnaIfNeeded();
5001
5786
  void this.mountKustomIfNeeded();
@@ -5427,10 +6212,12 @@ var _VioCheckout = class _VioCheckout extends LitElement {
5427
6212
  }
5428
6213
  if (method === "vipps") {
5429
6214
  this.vippsLoading = true;
6215
+ const redirect = beginPaymentRedirect({ method: "vipps", sponsorId: this.checkoutState?.sponsorId, source: "checkout" });
5430
6216
  void this.startVippsFromCheckout().catch((err) => {
5431
6217
  if (typeof console !== "undefined") {
5432
6218
  console.warn("[VioCheckout] Vipps payment could not start:", err);
5433
6219
  }
6220
+ redirect.fail(errorText(err));
5434
6221
  this.paymentError = friendlyPaymentError(errorText(err));
5435
6222
  }).finally(() => {
5436
6223
  this.vippsLoading = false;
@@ -5472,6 +6259,7 @@ var _VioCheckout = class _VioCheckout extends LitElement {
5472
6259
  const email = String(this.form.email ?? "").trim();
5473
6260
  const checkoutId = returnCheckoutId || this.checkoutState?.checkoutId;
5474
6261
  const providerOrderId = r.order?.orderId != null ? String(r.order.orderId) : "";
6262
+ this.setOrderSponsor(sponsorId, checkoutId);
5475
6263
  this.confirmedOrder = {
5476
6264
  items: [...this.items],
5477
6265
  currency: this.checkoutState?.currency ?? "",
@@ -5514,17 +6302,27 @@ var _VioCheckout = class _VioCheckout extends LitElement {
5514
6302
  * `visibilitychange`) or the element leaves, the event goes out at once
5515
6303
  * without the reference. Exactly one event per call. `afterwards` runs
5516
6304
  * right after the event, so a path keeps the order it always had (the cart
5517
- * is cleared after the event).
6305
+ * is cleared after the event). When that answer already has Vio's order
6306
+ * number, the event carries it too (`orderNumber`): it is the purchase's
6307
+ * `order_id`, and analytics need not ask again.
5518
6308
  */
5519
6309
  async emitPaymentSuccess(detail, checkoutId, sponsorId, afterwards) {
5520
6310
  let done = false;
5521
- const finish = (buyerRef) => {
6311
+ const sponsorAtStart = this.sponsorOfOrder(sponsorId, checkoutId);
6312
+ const finish = (found = {}) => {
5522
6313
  if (done) return;
5523
6314
  done = true;
6315
+ const named = this.sponsorOfOrder(sponsorId, checkoutId);
6316
+ const sponsor = named.sponsor ? named : sponsorAtStart;
6317
+ const withSponsor = sponsor.sponsor ? { ...detail, ...sponsor } : detail;
5524
6318
  const event = new CustomEvent("vio:payment-success", {
5525
6319
  bubbles: true,
5526
6320
  composed: true,
5527
- detail: buyerRef ? { ...detail, buyerRef } : detail
6321
+ detail: found.buyerRef || found.orderNumber ? {
6322
+ ...withSponsor,
6323
+ ...found.buyerRef ? { buyerRef: found.buyerRef } : {},
6324
+ ...found.orderNumber ? { orderNumber: found.orderNumber } : {}
6325
+ } : withSponsor
5528
6326
  });
5529
6327
  this.dispatchEvent(event);
5530
6328
  if (!this.isConnected && typeof window !== "undefined") {
@@ -5537,7 +6335,7 @@ var _VioCheckout = class _VioCheckout extends LitElement {
5537
6335
  };
5538
6336
  const known = buyerRefOf(detail);
5539
6337
  if (known || !checkoutId || !this.shouldAskBuyerRef(detail)) {
5540
- finish(known);
6338
+ finish({ buyerRef: known });
5541
6339
  return;
5542
6340
  }
5543
6341
  const settleEarly = () => finish();
@@ -5569,8 +6367,9 @@ var _VioCheckout = class _VioCheckout extends LitElement {
5569
6367
  * failed or has neither the reference nor an order yet (the webhook has not
5570
6368
  * written the checkout), again at 1.2 s — all within 2.5 s. An order
5571
6369
  * without a reference is a settled answer: the backend has none, asking
5572
- * again cannot help. `undefined` when the time is up, when the answer
5573
- * stopped mattering (`stopped()`) or on failure.
6370
+ * again cannot help. Whatever the answer said of the order number comes
6371
+ * back with it. `{}` when the time is up, when the answer stopped mattering
6372
+ * (`stopped()`) or on failure.
5574
6373
  */
5575
6374
  async askBuyerRef(checkoutId, sponsorId, stopped) {
5576
6375
  const started = Date.now();
@@ -5583,19 +6382,20 @@ var _VioCheckout = class _VioCheckout extends LitElement {
5583
6382
  for (const at of [0, _VioCheckout.BUYER_REF_RETRY_AT_MS]) {
5584
6383
  const wait = Math.min(at - (Date.now() - started), remaining());
5585
6384
  if (wait > 0) await Promise.race([new Promise((resolve) => setTimeout(resolve, wait)), timeUp]);
5586
- if (stopped() || remaining() <= 0) return void 0;
6385
+ if (stopped() || remaining() <= 0) return {};
5587
6386
  try {
5588
6387
  const reference = await Promise.race([
5589
6388
  Vio.checkout.getOrderReference(checkoutId, sponsorId || void 0),
5590
6389
  timeUp
5591
6390
  ]);
5592
- if (reference === null) return void 0;
5593
- if (reference.buyerRef) return reference.buyerRef;
5594
- if (reference.orderNumber !== null) return void 0;
6391
+ if (reference === null) return {};
6392
+ const order = reference.orderNumber ? { orderNumber: reference.orderNumber } : {};
6393
+ if (reference.buyerRef) return { buyerRef: reference.buyerRef, ...order };
6394
+ if (reference.orderNumber !== null) return order;
5595
6395
  } catch {
5596
6396
  }
5597
6397
  }
5598
- return void 0;
6398
+ return {};
5599
6399
  } finally {
5600
6400
  clearTimeout(deadline);
5601
6401
  }
@@ -5712,7 +6512,10 @@ var _VioCheckout = class _VioCheckout extends LitElement {
5712
6512
  >
5713
6513
  <div class="handle" @click=${this.close} aria-hidden="true"></div>
5714
6514
  <div class="topbar">
5715
- <h2 class="heading">${this.orderConfirmed ? "Takk!" : this.heading}</h2>
6515
+ <div class="topbar-title">
6516
+ <h2 class="heading">${this.orderConfirmed ? "Takk!" : this.heading}</h2>
6517
+ ${this.renderTopbarSponsor()}
6518
+ </div>
5716
6519
  <button class="close" @click=${this.close} aria-label="Lukk">×</button>
5717
6520
  </div>
5718
6521
 
@@ -5722,6 +6525,82 @@ var _VioCheckout = class _VioCheckout extends LitElement {
5722
6525
  </div>
5723
6526
  `;
5724
6527
  }
6528
+ /**
6529
+ * Remember whose order this is and how it is named right now, before the
6530
+ * cart that names it is cleared. Whose: the sponsor this tab remembered for
6531
+ * the checkout when it was created (exact for that checkout id), else the
6532
+ * one given (the return URL's `vio_sponsor`, the state it was paid in),
6533
+ * else the open state's — a return without `vio_sponsor` (Qliro, Walley)
6534
+ * re-opens the state with whichever cart comes first, which is a guess.
6535
+ */
6536
+ setOrderSponsor(sponsorId, checkoutId) {
6537
+ try {
6538
+ const checkout = checkoutId || void 0;
6539
+ const id = rememberedCheckoutSponsor(checkout)?.sponsorId ?? (sponsorId && sponsorId > 0 ? sponsorId : void 0) ?? this.checkoutState?.sponsorId;
6540
+ if (!id) return;
6541
+ const named = checkoutSponsorOf(resolveSponsorIdentity(id, checkout));
6542
+ const previous = this.orderSponsor;
6543
+ const snapshot = named ?? (previous && previous.sponsorId === id ? previous.snapshot : void 0);
6544
+ this.orderSponsor = { sponsorId: id, ...checkout ? { checkoutId: checkout } : {}, ...snapshot ? { snapshot } : {} };
6545
+ } catch {
6546
+ }
6547
+ }
6548
+ /**
6549
+ * Who this checkout is with, as a shopper sees it: the order being
6550
+ * confirmed / verified (`orderSponsor`), else the open checkout's own
6551
+ * sponsor (`checkoutState.sponsorId`) — named by `Vio.sponsorIdentity`,
6552
+ * which falls back to this tab's memory of the checkout and the cart; the
6553
+ * name taken before the cart was cleared last. Never the page's
6554
+ * attribution. `null` when nothing names it.
6555
+ */
6556
+ checkoutSponsorIdentity() {
6557
+ const o = this.orderSponsor;
6558
+ if (o) return checkoutSponsorOf(resolveSponsorIdentity(o.sponsorId, o.checkoutId)) ?? o.snapshot ?? null;
6559
+ const s = this.checkoutState;
6560
+ return s?.sponsorId ? checkoutSponsorOf(resolveSponsorIdentity(s.sponsorId, s.checkoutId)) ?? null : null;
6561
+ }
6562
+ /**
6563
+ * `{ sponsor }` for the payment event — the same sponsor the screen names
6564
+ * (`orderSponsor`, set by every path right before it emits): named now, else
6565
+ * as named before the cart was cleared; `{}` when nothing names it.
6566
+ */
6567
+ sponsorOfOrder(sponsorId, checkoutId) {
6568
+ try {
6569
+ const o = this.orderSponsor;
6570
+ const sameOrder = !!o && (o.checkoutId ?? void 0) === (checkoutId || void 0);
6571
+ const id = sameOrder ? o.sponsorId : sponsorId > 0 ? sponsorId : void 0;
6572
+ const live = sponsorDetail(id, checkoutId);
6573
+ if (live.sponsor) return live;
6574
+ if (sameOrder && o.snapshot) return { sponsor: o.snapshot };
6575
+ } catch {
6576
+ }
6577
+ return {};
6578
+ }
6579
+ /**
6580
+ * Ask once per checkout for the bootstrap when it is not there yet and a
6581
+ * sponsor has to be named (a redirect return renders before it), then
6582
+ * render again — the name and the logo appear when it lands.
6583
+ */
6584
+ watchSponsorBootstrap() {
6585
+ if (this.sponsorBootstrapAsked || !this.open || Vio.bootstrapCache) return;
6586
+ if (!this.orderSponsor && !this.checkoutState?.sponsorId) return;
6587
+ this.sponsorBootstrapAsked = true;
6588
+ void whenBootstrapped().then(() => this.requestUpdate());
6589
+ }
6590
+ /**
6591
+ * The checkout's sponsor in the header — «Du handler hos <logo|name>»
6592
+ * («Du har handlet hos» on a provider's receipt). Not on our own
6593
+ * confirmation, which says it in its body.
6594
+ */
6595
+ renderTopbarSponsor() {
6596
+ if (this.orderConfirmed) return nothing;
6597
+ const sponsor = this.checkoutSponsorIdentity();
6598
+ const mark = renderSponsorMark(this, sponsor, { logo: "topbar-sponsor-logo", name: "topbar-sponsor-name" });
6599
+ if (mark === nothing) return nothing;
6600
+ return html`<p class="topbar-sponsor" part="sponsor">
6601
+ <span>${this.providerReceipt ? "Du har handlet hos" : "Du handler hos"}</span> ${mark}
6602
+ </p>`;
6603
+ }
5725
6604
  /**
5726
6605
  * The provider's receipt on its own. It must not depend on the cart: the
5727
6606
  * payment step is rendered from the cart, and clearing the cart after a
@@ -5749,6 +6628,7 @@ var _VioCheckout = class _VioCheckout extends LitElement {
5749
6628
  this.confirmedMethod = method;
5750
6629
  this.open = true;
5751
6630
  const checkoutId = returnCheckoutId || this.checkoutState?.checkoutId;
6631
+ this.setOrderSponsor(sponsorId, checkoutId);
5752
6632
  void this.emitPaymentSuccess(
5753
6633
  { method, sponsorId, result, checkoutId },
5754
6634
  checkoutId,
@@ -5777,19 +6657,24 @@ var _VioCheckout = class _VioCheckout extends LitElement {
5777
6657
  details.push(["Betalingsm\xE5te", o?.paidWith || this.methodLabel(this.confirmedMethod)]);
5778
6658
  if (o?.providerRef) details.push([o.providerRef.label, o.providerRef.value]);
5779
6659
  if (email) details.push(["Kvittering til", email]);
6660
+ const sponsor = this.checkoutSponsorIdentity();
5780
6661
  return html`
5781
- <section class="confirmation">
6662
+ <section class="confirmation" part="confirmation">
6663
+ ${usableLogo(sponsor) ? html`<div class="confirm-brand" part="sponsor">
6664
+ ${renderSponsorMark(this, sponsor, { logo: "confirm-brand-logo", name: "confirm-brand-name" })}
6665
+ </div>` : ""}
5782
6666
  <header class="confirm-head">
5783
6667
  <div class="confirm-check" aria-hidden="true">✓</div>
5784
6668
  <div>
5785
- <h3 class="confirm-title">Takk for bestillingen!</h3>
6669
+ <h3 class="confirm-title" part="confirm-title">Takk for bestillingen!</h3>
6670
+ ${sponsor?.name ? html`<p class="confirm-sponsor" part="confirm-sponsor">Du har handlet hos <strong>${sponsor.name}</strong>.</p>` : ""}
5786
6671
  <p class="confirm-text">
5787
6672
  Betalingen er bekreftet.${email ? html` Du får en ordrebekreftelse på e-post.` : ""}
5788
6673
  </p>
5789
6674
  </div>
5790
6675
  </header>
5791
6676
 
5792
- <dl class="confirm-details">
6677
+ <dl class="confirm-details" part="confirm-details">
5793
6678
  ${details.map(
5794
6679
  ([k, v]) => html`<div><dt>${k}</dt><dd class=${k === o?.providerRef?.label ? "ref" : ""}>${v}</dd></div>`
5795
6680
  )}
@@ -6199,12 +7084,12 @@ var _VioCheckout = class _VioCheckout extends LitElement {
6199
7084
  for (const delay of _VioCheckout.NEXI_DETAILS_DELAYS_MS) {
6200
7085
  if (delay) await new Promise((r) => setTimeout(r, delay));
6201
7086
  const o = await Vio.checkout.getNexiOrder(checkoutId, sponsorId).catch(() => null);
6202
- const current = this.confirmedOrder;
6203
- if (!current || current.providerRef?.value !== paymentId) return;
7087
+ const current2 = this.confirmedOrder;
7088
+ if (!current2 || current2.providerRef?.value !== paymentId) return;
6204
7089
  if (!o) continue;
6205
7090
  const paidWith = o.payment_method ? o.card_last4 ? `${o.payment_method} \u2022\u2022\u2022\u2022 ${o.card_last4}` : o.payment_method : void 0;
6206
7091
  this.confirmedOrder = {
6207
- ...current,
7092
+ ...current2,
6208
7093
  ...paidWith ? { paidWith } : {},
6209
7094
  ...o.email ? { email: o.email } : {}
6210
7095
  };
@@ -6232,15 +7117,15 @@ var _VioCheckout = class _VioCheckout extends LitElement {
6232
7117
  const options = this.nexiShipping?.options ?? [];
6233
7118
  if (options.length < 2) return "";
6234
7119
  const currency = this.checkoutState?.currency || getGlobalCurrency();
6235
- const current = this.nexiShipping?.shipping_id;
7120
+ const current2 = this.nexiShipping?.shipping_id;
6236
7121
  return html`
6237
7122
  <div class="ship-select nexi-ship" role="radiogroup" aria-label="Frakt">
6238
7123
  ${options.map(
6239
7124
  (o) => html`
6240
7125
  <button
6241
- class="ship-opt ${current === o.id ? "active" : ""}"
7126
+ class="ship-opt ${current2 === o.id ? "active" : ""}"
6242
7127
  role="radio"
6243
- aria-checked=${current === o.id}
7128
+ aria-checked=${current2 === o.id}
6244
7129
  ?disabled=${this.nexiPicking}
6245
7130
  @click=${() => void this.onPickNexiShipping(o.id)}
6246
7131
  >
@@ -7075,6 +7960,9 @@ __decorateClass([
7075
7960
  __decorateClass([
7076
7961
  state()
7077
7962
  ], _VioCheckout.prototype, "verifyingReturn", 2);
7963
+ __decorateClass([
7964
+ state()
7965
+ ], _VioCheckout.prototype, "orderSponsor", 2);
7078
7966
  __decorateClass([
7079
7967
  state()
7080
7968
  ], _VioCheckout.prototype, "confirmedOrder", 2);
@@ -7211,5 +8099,5 @@ function registerVioElements() {
7211
8099
  }
7212
8100
 
7213
8101
  export { VioCart, VioCheckout, VioProduct, VioProductCarousel, VioProductDetail, VioTokens, applyVioTheme, registerVioElements };
7214
- //# sourceMappingURL=chunk-6Q2M3MIK.js.map
7215
- //# sourceMappingURL=chunk-6Q2M3MIK.js.map
8102
+ //# sourceMappingURL=chunk-Q2NALS7P.js.map
8103
+ //# sourceMappingURL=chunk-Q2NALS7P.js.map