@vio-live/web-sdk 0.29.0 → 0.32.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,4 +1,4 @@
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-AG4G2S2M.js';
1
+ import { Vio, __decorateClass, primaryImageUrl, displayPrice, toCampaignId, getGlobalCurrency, ensureVippsButton, formatPrice, buyerRefOf, nexiReturnPaymentId, readNexiPending, kustomReturnOutcome, KUSTOM_RETURN_QUERY_KEYS, storageGet, ADYEN_RETURN_METHOD, adyenReturnParams, ADYEN_RETURN_QUERY_KEYS, adyenReturnOutcome, getGlobalCountryCode, isValidPhone, gatewayHasBuyerRef, rememberedCheckoutSponsor, checkoutSponsorOf, AdyenOriginError, normalizeSlotId, autoSlotDisplayName, autoSlotId, sponsorIdentityFrom, Configuration, normalizeScope, campaignCandidates } from './chunk-IR4XYTZL.js';
2
2
  import { LitElement, css, html, nothing } from 'lit';
3
3
  import { property, state, query } from 'lit/decorators.js';
4
4
 
@@ -164,6 +164,13 @@ var THEME_KEY_TO_CSS_VAR = {
164
164
  detailImageZoom: "--vio-detail-image-zoom",
165
165
  detailImageInset: "--vio-detail-image-inset",
166
166
  detailImageBg: "--vio-detail-image-bg",
167
+ detailGallery: "--vio-detail-gallery",
168
+ carouselLayout: "--vio-carousel-layout",
169
+ detailCarouselPeek: "--vio-detail-carousel-peek",
170
+ detailDescriptionMode: "--vio-detail-description-mode",
171
+ detailDescriptionPreviewLines: "--vio-detail-description-preview-lines",
172
+ detailHighlightsDisplay: "--vio-detail-highlights-display",
173
+ detailShippingReturnsDisplay: "--vio-detail-shipping-returns-display",
167
174
  detailThumbSize: "--vio-detail-thumb-size",
168
175
  detailThumbGap: "--vio-detail-thumb-gap",
169
176
  detailThumbRadius: "--vio-detail-thumb-radius",
@@ -176,6 +183,18 @@ var THEME_KEY_TO_CSS_VAR = {
176
183
  detailButtonRadius: "--vio-detail-button-radius",
177
184
  detailBuyPosition: "--vio-detail-buy-position",
178
185
  detailBuyShadow: "--vio-detail-buy-shadow",
186
+ cardFont: "--vio-card-font",
187
+ cardBorder: "--vio-card-border",
188
+ cardImageAspect: "--vio-card-image-aspect",
189
+ cardImageFit: "--vio-card-image-fit",
190
+ cardImageBg: "--vio-card-image-bg",
191
+ cardImagePadding: "--vio-card-image-padding",
192
+ cardMetaPadding: "--vio-card-meta-padding",
193
+ cardPriceFont: "--vio-card-price-font",
194
+ cardAccent: "--vio-card-accent",
195
+ cardBarRadius: "--vio-card-bar-radius",
196
+ cardDescriptionLines: "--vio-card-description-lines",
197
+ cardRetailerOrder: "--vio-card-retailer-order",
179
198
  cartWidth: "--vio-cart-width",
180
199
  checkoutWidth: "--vio-checkout-width",
181
200
  confirmLogoHeight: "--vio-confirm-logo-height",
@@ -359,6 +378,10 @@ var VioProduct = class extends LitElement {
359
378
  this.brand = "";
360
379
  this.name = "";
361
380
  this.price = "";
381
+ this.description = "";
382
+ this.showDescription = false;
383
+ this.buttonStyle = "icon";
384
+ this.buttonLabel = "Kj\xF8p";
362
385
  this.retailer = "";
363
386
  this.imageUrl = "";
364
387
  this.productId = "";
@@ -427,20 +450,29 @@ var VioProduct = class extends LitElement {
427
450
  box-sizing: border-box;
428
451
  width: 100%;
429
452
  cursor: pointer;
430
- font-family: var(--vio-font-sans, -apple-system, sans-serif);
453
+ font-family: var(--vio-card-font, var(--vio-font-sans, -apple-system, sans-serif));
431
454
  color: var(--vio-color-text, #0a0a0a);
432
455
  -webkit-tap-highlight-color: transparent;
433
456
  }
457
+ /* Every --vio-card-* below defaults to the look the card always had; a
458
+ host restyles the card with them (they cross the carousel's shadow
459
+ root, ::part would not). E.g. a framed card with a buy button:
460
+ --vio-card-border, --vio-card-meta-padding, --vio-card-image-fit: contain. */
434
461
  .card {
435
462
  display: flex;
436
463
  flex-direction: column;
437
464
  height: 100%;
465
+ box-sizing: border-box;
466
+ border: var(--vio-card-border, none);
467
+ background: var(--vio-card-bg, transparent);
468
+ transition: border-color 0.2s ease;
438
469
  }
470
+ :host(:hover) .card { border-color: var(--vio-card-border-hover-color, var(--vio-color-border, #e5e5e5)); }
439
471
  .image {
440
472
  position: relative;
441
- aspect-ratio: 1 / 1;
473
+ aspect-ratio: var(--vio-card-image-aspect, 1 / 1);
442
474
  width: 100%;
443
- background-color: var(--vio-color-surface-muted, #f2f2f2);
475
+ background-color: var(--vio-card-image-bg, var(--vio-color-surface-muted, #f2f2f2));
444
476
  display: flex;
445
477
  align-items: center;
446
478
  justify-content: center;
@@ -455,8 +487,8 @@ var VioProduct = class extends LitElement {
455
487
  position: absolute;
456
488
  right: 10px;
457
489
  bottom: 10px;
458
- width: 40px;
459
- height: 40px;
490
+ width: var(--vio-card-add-size, 40px);
491
+ height: var(--vio-card-add-size, 40px);
460
492
  border-radius: 50%;
461
493
  background: rgba(255, 255, 255, 0.96);
462
494
  border: none;
@@ -476,7 +508,7 @@ var VioProduct = class extends LitElement {
476
508
  .add-btn.in-cart { background: #0a0a0a; color: #fff; }
477
509
  .add-btn.in-cart:hover { background: #222; }
478
510
  .add-btn.pop { animation: vio-add-pop 0.42s cubic-bezier(0.34, 1.56, 0.64, 1); }
479
- .add-btn svg { width: 18px; height: 18px; }
511
+ .add-btn svg { width: var(--vio-card-add-icon-size, 18px); height: var(--vio-card-add-icon-size, 18px); }
480
512
  .add-btn .add-count { font-size: 15px; font-weight: 700; line-height: 1; }
481
513
  @keyframes vio-add-pop {
482
514
  0% { transform: scale(1); }
@@ -487,47 +519,87 @@ var VioProduct = class extends LitElement {
487
519
  .image img {
488
520
  width: 100%;
489
521
  height: 100%;
490
- object-fit: cover;
522
+ box-sizing: border-box;
523
+ padding: var(--vio-card-image-padding, 0);
524
+ object-fit: var(--vio-card-image-fit, cover);
491
525
  display: block;
492
526
  }
493
527
  .meta {
494
- padding-top: 14px;
528
+ padding: var(--vio-card-meta-padding, 14px 0 0);
495
529
  display: flex;
496
530
  flex-direction: column;
497
531
  flex: 1;
498
532
  }
499
533
  .brand {
500
- font-size: var(--vio-size-xs, 12px);
501
- letter-spacing: var(--vio-tracking-label, 0.1em);
534
+ font-size: var(--vio-card-brand-size, var(--vio-size-xs, 12px));
535
+ letter-spacing: var(--vio-card-brand-tracking, var(--vio-tracking-label, 0.1em));
502
536
  text-transform: uppercase;
503
- color: var(--vio-color-text-secondary, #666);
504
- font-weight: 500;
505
- margin-bottom: 6px;
537
+ color: var(--vio-card-brand-color, var(--vio-color-text-secondary, #666));
538
+ font-weight: var(--vio-card-brand-weight, 500);
539
+ margin-bottom: var(--vio-card-brand-gap, 6px);
506
540
  }
507
541
  .name {
508
- font-size: var(--vio-size-md, 16px);
509
- font-weight: 500;
542
+ font-family: var(--vio-card-name-font, inherit);
543
+ font-size: var(--vio-card-name-size, var(--vio-size-md, 16px));
544
+ font-weight: var(--vio-card-name-weight, 500);
510
545
  line-height: 1.3;
511
- margin: 0 0 10px;
546
+ margin: 0 0 var(--vio-card-name-gap, 10px);
512
547
  display: -webkit-box;
513
548
  -webkit-line-clamp: 2;
514
549
  line-clamp: 2;
515
550
  -webkit-box-orient: vertical;
516
551
  overflow: hidden;
517
552
  }
553
+ /* show-description: cut after --vio-card-description-lines (4). */
554
+ .description {
555
+ display: -webkit-box;
556
+ -webkit-line-clamp: var(--vio-card-description-lines, 4);
557
+ line-clamp: var(--vio-card-description-lines, 4);
558
+ -webkit-box-orient: vertical;
559
+ overflow: hidden;
560
+ font-size: var(--vio-card-description-size, 14px);
561
+ line-height: 1.45;
562
+ color: var(--vio-card-description-color, var(--vio-color-text-secondary, #737373));
563
+ margin: 0 0 var(--vio-card-description-gap, 12px);
564
+ }
518
565
  .price {
519
- font-size: var(--vio-size-md, 16px);
520
- font-weight: 700;
521
- margin-bottom: 16px;
566
+ font-family: var(--vio-card-price-font, inherit);
567
+ font-size: var(--vio-card-price-size, var(--vio-size-md, 16px));
568
+ font-weight: var(--vio-card-price-weight, 700);
569
+ margin-bottom: var(--vio-card-price-gap, 16px);
522
570
  }
571
+ /* Who sells it. --vio-card-retailer-order: -1 lifts it above the name
572
+ (where a brand line goes), for catalogues without a brand of their own. */
523
573
  .retailer {
524
- font-size: var(--vio-size-xs, 12px);
525
- letter-spacing: var(--vio-tracking-label, 0.1em);
574
+ display: var(--vio-card-retailer-display, block);
575
+ order: var(--vio-card-retailer-order, 0);
576
+ font-size: var(--vio-card-retailer-size, var(--vio-size-xs, 12px));
577
+ letter-spacing: var(--vio-card-retailer-tracking, var(--vio-tracking-label, 0.1em));
526
578
  text-transform: uppercase;
527
- color: var(--vio-color-text-tertiary, #999);
528
- font-weight: 500;
579
+ color: var(--vio-card-retailer-color, var(--vio-color-text-tertiary, #999));
580
+ font-weight: var(--vio-card-retailer-weight, 500);
581
+ margin: var(--vio-card-retailer-margin, auto 0 0);
582
+ }
583
+ /* button-style="bar": the Vev card's bar — full width under the price,
584
+ uppercase, the accent colour, 6px corners (--vio-card-bar-radius). */
585
+ .bar-btn {
586
+ width: 100%;
529
587
  margin-top: auto;
588
+ padding: 14px;
589
+ border: none;
590
+ border-radius: var(--vio-card-bar-radius, 6px);
591
+ background: var(--vio-card-accent, var(--vio-color-text, #0a0a0a));
592
+ color: var(--vio-card-accent-text, #fff);
593
+ font-family: inherit;
594
+ font-size: 13px;
595
+ font-weight: 600;
596
+ letter-spacing: 0.08em;
597
+ text-transform: uppercase;
598
+ cursor: pointer;
599
+ transition: opacity 0.15s ease;
530
600
  }
601
+ .bar-btn:hover { opacity: 0.88; }
602
+ .bar-btn.pop { animation: vio-add-pop 0.42s cubic-bezier(0.34, 1.56, 0.64, 1); }
531
603
  `;
532
604
  }
533
605
  connectedCallback() {
@@ -644,7 +716,7 @@ var VioProduct = class extends LitElement {
644
716
  >
645
717
  <div class="image">
646
718
  ${this.imageUrl ? html`<img src=${this.imageUrl} alt=${this.name || this.brand} loading="lazy" />` : ""}
647
- <button
719
+ ${this.buttonStyle === "icon" ? html`<button
648
720
  class="add-btn ${this.inCart ? "in-cart" : ""} ${this.justAdded ? "pop" : ""}"
649
721
  @click=${this.onAdd}
650
722
  aria-label=${this.inCart ? "I handlekurven \u2013 legg til en til" : "Legg i kurv"}
@@ -657,14 +729,18 @@ var VioProduct = class extends LitElement {
657
729
  <line x1="3" y1="6" x2="21" y2="6"></line>
658
730
  <path d="M16 10a4 4 0 0 1-8 0"></path>
659
731
  </svg>`}
660
- </button>
732
+ </button>` : ""}
661
733
  </div>
662
734
  ${this.hideMeta ? "" : html`
663
735
  <div class="meta">
664
736
  ${this.brand ? html`<div class="brand">${this.brand}</div>` : ""}
665
737
  ${this.name ? html`<div class="name">${this.name}</div>` : ""}
738
+ ${this.showDescription && this.description ? html`<p class="description">${this.description}</p>` : ""}
666
739
  ${this.price ? html`<div class="price">${this.price}</div>` : ""}
667
740
  ${this.retailer ? html`<div class="retailer">${this.retailer}</div>` : ""}
741
+ ${this.buttonStyle === "bar" ? html`<button class="bar-btn ${this.justAdded ? "pop" : ""}" @click=${this.onAdd}>
742
+ ${this.inCart ? `${this.buttonLabel} \xB7 ${this.cartQty}` : this.buttonLabel}
743
+ </button>` : ""}
668
744
  </div>`}
669
745
  </div>
670
746
  `;
@@ -679,6 +755,18 @@ __decorateClass([
679
755
  __decorateClass([
680
756
  property({ type: String })
681
757
  ], VioProduct.prototype, "price", 2);
758
+ __decorateClass([
759
+ property({ type: String })
760
+ ], VioProduct.prototype, "description", 2);
761
+ __decorateClass([
762
+ property({ type: Boolean, attribute: "show-description" })
763
+ ], VioProduct.prototype, "showDescription", 2);
764
+ __decorateClass([
765
+ property({ type: String, attribute: "button-style" })
766
+ ], VioProduct.prototype, "buttonStyle", 2);
767
+ __decorateClass([
768
+ property({ type: String, attribute: "button-label" })
769
+ ], VioProduct.prototype, "buttonLabel", 2);
682
770
  __decorateClass([
683
771
  property({ type: String })
684
772
  ], VioProduct.prototype, "retailer", 2);
@@ -797,6 +885,12 @@ var VioProductCarousel = class extends LitElement {
797
885
  this.sponsorId = 0;
798
886
  this.productRefs = "";
799
887
  this.currency = "NOK";
888
+ this.layout = "";
889
+ this.showDescription = false;
890
+ this.descriptions = {};
891
+ this.buttonStyle = "icon";
892
+ this.buttonLabel = "Kj\xF8p";
893
+ this.resolvedLayout = "carousel";
800
894
  this.vioSlot = "";
801
895
  this.vioSlotName = "";
802
896
  this.vioScope = "";
@@ -810,7 +904,11 @@ var VioProductCarousel = class extends LitElement {
810
904
  this.fetchError = "";
811
905
  // ── Analytics (auto) ──────────────────────────────────────────────
812
906
  // component_impression on the contract rule (≥50% visible ≥1s, once per
813
- // session) + view_item_list with the rendered products. No-ops unless
907
+ // session) with the products shown in `commerce.items` (ADR-0041: the
908
+ // collector counts per-product impressions from them) + view_item_list with
909
+ // the rendered products. While products load the impression is not
910
+ // observed: it starts once they are here (or the fetch failed — slotted
911
+ // fallback is still an impression, without items). No-ops unless
814
912
  // Vio.analytics.start() ran.
815
913
  this.unobserveImpression = null;
816
914
  this.impressed = false;
@@ -919,6 +1017,21 @@ var VioProductCarousel = class extends LitElement {
919
1017
  .scroller { padding: 0 var(--vio-space-lg, 24px) 8px; }
920
1018
  }
921
1019
  .scroller::-webkit-scrollbar { display: none; }
1020
+ /* layout="list": one card under the other, as wide as the column. */
1021
+ .viewport.list .scroller {
1022
+ flex-direction: column;
1023
+ align-items: stretch;
1024
+ gap: var(--vio-carousel-list-gap, 32px);
1025
+ max-width: var(--vio-carousel-list-max-width, 480px);
1026
+ margin: 0 auto;
1027
+ padding: 0 var(--vio-space-lg, 24px);
1028
+ overflow: visible;
1029
+ scroll-snap-type: none;
1030
+ }
1031
+ .viewport.list .scroller vio-product,
1032
+ .viewport.list ::slotted(*),
1033
+ .viewport.list .skeleton { flex: 0 0 auto; width: 100%; }
1034
+ .viewport.list .nav-btn { display: none; }
922
1035
  ::slotted(*),
923
1036
  vio-product {
924
1037
  flex: 0 0 240px;
@@ -991,13 +1104,32 @@ var VioProductCarousel = class extends LitElement {
991
1104
  }
992
1105
  `;
993
1106
  }
1107
+ overrideFor(p) {
1108
+ const own = this.descriptions?.[String(p.id)];
1109
+ return typeof own === "string" ? own : void 0;
1110
+ }
1111
+ descriptionFor(p) {
1112
+ return this.overrideFor(p) ?? cardDescription(p.description);
1113
+ }
1114
+ /** The editor's text always shows (unless empty); the catalogue's only with show-description. */
1115
+ showsDescription(p) {
1116
+ const own = this.overrideFor(p);
1117
+ return own !== void 0 ? own.trim() !== "" : this.showDescription;
1118
+ }
1119
+ willUpdate(changed) {
1120
+ if (changed.has("layout") || !this.hasUpdated) {
1121
+ const fromTheme = typeof getComputedStyle === "function" ? getComputedStyle(this).getPropertyValue("--vio-carousel-layout") : "";
1122
+ const value = (this.layout || fromTheme).trim().replace(/^["']|["']$/g, "").toLowerCase();
1123
+ this.resolvedLayout = value === "list" ? "list" : "carousel";
1124
+ }
1125
+ }
994
1126
  connectedCallback() {
995
1127
  super.connectedCallback();
996
1128
  this.slotRegistration.connect(this.slotAttributes());
997
- this.observeForAnalytics();
998
1129
  if (this.productRefs.trim() || this.productIds.trim()) {
999
1130
  void this.autoFetch();
1000
1131
  }
1132
+ this.observeForAnalytics();
1001
1133
  }
1002
1134
  disconnectedCallback() {
1003
1135
  this.unobserveImpression?.();
@@ -1031,6 +1163,10 @@ var VioProductCarousel = class extends LitElement {
1031
1163
  }
1032
1164
  this.isLoading = true;
1033
1165
  this.fetchError = "";
1166
+ if (!this.impressed) {
1167
+ this.unobserveImpression?.();
1168
+ this.unobserveImpression = null;
1169
+ }
1034
1170
  try {
1035
1171
  if (this.productRefs.trim() === "" && !this.commerceKey && this.sponsorId <= 0 && !Vio.bootstrapCache) {
1036
1172
  try {
@@ -1074,10 +1210,11 @@ var VioProductCarousel = class extends LitElement {
1074
1210
  this.fetchError = err instanceof Error ? err.message : String(err);
1075
1211
  } finally {
1076
1212
  this.isLoading = false;
1213
+ this.observeForAnalytics();
1077
1214
  }
1078
1215
  }
1079
1216
  observeForAnalytics() {
1080
- if (this.unobserveImpression) return;
1217
+ if (this.unobserveImpression || this.isLoading || !this.isConnected) return;
1081
1218
  try {
1082
1219
  this.unobserveImpression = Vio.analytics.observeImpression(
1083
1220
  this,
@@ -1092,7 +1229,8 @@ var VioProductCarousel = class extends LitElement {
1092
1229
  onImpression: () => {
1093
1230
  this.impressed = true;
1094
1231
  this.maybeTrackList();
1095
- }
1232
+ },
1233
+ commerce: () => this.listCommerce()
1096
1234
  }
1097
1235
  );
1098
1236
  } catch {
@@ -1113,19 +1251,25 @@ var VioProductCarousel = class extends LitElement {
1113
1251
  locationId: this.effectiveSlot || this.label || void 0,
1114
1252
  ...this.attribution()
1115
1253
  },
1116
- commerce: {
1117
- items: entries.map(({ product, sponsorId: sponsorId2 }) => ({
1118
- productId: product.id,
1119
- name: product.title,
1120
- price: Number(product.price?.amount_incl_taxes ?? product.price?.amount) || void 0
1121
- })),
1122
- currency: entries[0]?.product.price?.currency_code
1123
- },
1254
+ commerce: this.listCommerce() ?? void 0,
1124
1255
  props: this.label ? { list_name: this.label } : void 0
1125
1256
  });
1126
1257
  } catch {
1127
1258
  }
1128
1259
  }
1260
+ /** The products on show as analytics lines (impression and view_item_list); null before they load. */
1261
+ listCommerce() {
1262
+ const entries = this.fetched ?? [];
1263
+ if (entries.length === 0) return null;
1264
+ return {
1265
+ items: entries.map(({ product }) => ({
1266
+ productId: product.id,
1267
+ name: product.title,
1268
+ price: Number(product.price?.amount_incl_taxes ?? product.price?.amount) || void 0
1269
+ })),
1270
+ currency: entries[0]?.product.price?.currency_code
1271
+ };
1272
+ }
1129
1273
  parseRefs(raw) {
1130
1274
  return raw.split(",").map((pair) => pair.trim()).filter((s) => s.length > 0).map((pair) => {
1131
1275
  const [sidStr, pidStr] = pair.split(":");
@@ -1165,6 +1309,10 @@ var VioProductCarousel = class extends LitElement {
1165
1309
  name=${p.title}
1166
1310
  price=${displayPrice(p)}
1167
1311
  retailer=${retailer}
1312
+ description=${this.descriptionFor(p)}
1313
+ ?show-description=${this.showsDescription(p)}
1314
+ button-style=${this.buttonStyle}
1315
+ button-label=${this.buttonLabel}
1168
1316
  image-url=${img}
1169
1317
  ?has-variants=${(p.variants?.length ?? 0) > 1}
1170
1318
  .vioSlot=${this.effectiveSlot}
@@ -1190,7 +1338,7 @@ var VioProductCarousel = class extends LitElement {
1190
1338
  </div>
1191
1339
  ${this.seeAllHref ? html`<a class="see-all" href=${this.seeAllHref}>${this.seeAllLabel}</a>` : ""}
1192
1340
  </div>
1193
- <div class="viewport">
1341
+ <div class="viewport ${this.resolvedLayout}">
1194
1342
  <button
1195
1343
  class="nav-btn prev"
1196
1344
  @click=${() => this.scrollByDirection(-1)}
@@ -1241,6 +1389,24 @@ __decorateClass([
1241
1389
  __decorateClass([
1242
1390
  property({ type: String })
1243
1391
  ], VioProductCarousel.prototype, "currency", 2);
1392
+ __decorateClass([
1393
+ property({ type: String, reflect: true })
1394
+ ], VioProductCarousel.prototype, "layout", 2);
1395
+ __decorateClass([
1396
+ property({ type: Boolean, attribute: "show-description" })
1397
+ ], VioProductCarousel.prototype, "showDescription", 2);
1398
+ __decorateClass([
1399
+ property({ type: Object })
1400
+ ], VioProductCarousel.prototype, "descriptions", 2);
1401
+ __decorateClass([
1402
+ property({ type: String, attribute: "button-style" })
1403
+ ], VioProductCarousel.prototype, "buttonStyle", 2);
1404
+ __decorateClass([
1405
+ property({ type: String, attribute: "button-label" })
1406
+ ], VioProductCarousel.prototype, "buttonLabel", 2);
1407
+ __decorateClass([
1408
+ state()
1409
+ ], VioProductCarousel.prototype, "resolvedLayout", 2);
1244
1410
  __decorateClass([
1245
1411
  property({ type: String, attribute: "vio-slot" })
1246
1412
  ], VioProductCarousel.prototype, "vioSlot", 2);
@@ -1274,6 +1440,10 @@ __decorateClass([
1274
1440
  __decorateClass([
1275
1441
  state()
1276
1442
  ], VioProductCarousel.prototype, "fetchError", 2);
1443
+ function cardDescription(description) {
1444
+ if (!description) return "";
1445
+ return description.replace(/<[^>]+>/g, " ").replace(/&nbsp;/g, " ").replace(/&amp;/g, "&").replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/\s+/g, " ").trim();
1446
+ }
1277
1447
 
1278
1448
  // src/ui/payment-redirect.ts
1279
1449
  var OVERLAY_CLASS = "vio-redirect-overlay";
@@ -1381,7 +1551,7 @@ var NO_EXPRESS_BUTTON_METHODS = [
1381
1551
  ...EMBEDDED_METHODS,
1382
1552
  ...FORM_FIRST_WIDGET_METHODS
1383
1553
  ];
1384
- var COLLECTS_OWN_ADDRESS = [...EMBEDDED_METHODS, "vipps"];
1554
+ var COLLECTS_OWN_ADDRESS = [...EMBEDDED_METHODS];
1385
1555
  var isEmbeddedMethod = (name) => EMBEDDED_METHODS.includes(normalizeMethodName(name));
1386
1556
  var isFormFirstWidgetMethod = (name) => FORM_FIRST_WIDGET_METHODS.includes(normalizeMethodName(name));
1387
1557
  var collectsOwnAddress = (name) => COLLECTS_OWN_ADDRESS.includes(normalizeMethodName(name));
@@ -1595,6 +1765,8 @@ var BottomSheet = class {
1595
1765
  this.locked = false;
1596
1766
  this.drag = null;
1597
1767
  this.dragEndedAt = -Infinity;
1768
+ /** A finger down in the content, not yet known to be a pull (down) or a scroll. */
1769
+ this.pull = null;
1598
1770
  this.onPointerDown = (e) => {
1599
1771
  if (!e.isPrimary || e.pointerType === "mouse" && e.button !== 0) return;
1600
1772
  if (this.drag || !isPhoneSheet()) return;
@@ -1620,45 +1792,54 @@ var BottomSheet = class {
1620
1792
  if (!drag.moving) {
1621
1793
  if (dy < -SLOP) return this.abandon();
1622
1794
  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));
1795
+ this.startMoving(drag);
1640
1796
  }
1797
+ this.follow(drag, e.clientY, e.timeStamp);
1641
1798
  if (e.cancelable) e.preventDefault();
1642
1799
  };
1643
1800
  this.onPointerUp = (e) => {
1644
1801
  const drag = this.drag;
1645
1802
  if (!drag || e.pointerId !== drag.pointerId) return;
1646
1803
  this.stopTracking();
1647
- this.drag = null;
1648
- if (!drag.moving) return;
1649
- this.dragEndedAt = performance.now();
1804
+ this.finish(drag);
1805
+ };
1806
+ // ── Pull from the content (touch) ─────────────────────────────────────────
1807
+ this.onTouchStart = (e) => {
1808
+ this.pull = null;
1809
+ if (this.drag || e.touches.length !== 1 || !isPhoneSheet()) return;
1650
1810
  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 = "";
1811
+ if (!sheet || !e.composedPath().includes(sheet)) return;
1812
+ if (inZone(e, sheet, this.opts.dragZones)) return;
1813
+ const t = e.touches[0];
1814
+ if (t) this.pull = { startX: t.clientX, startY: t.clientY };
1815
+ };
1816
+ this.onTouchMove = (e) => {
1817
+ const t = e.touches[0];
1818
+ if (!t) return;
1819
+ if (this.pull) {
1820
+ const dx = t.clientX - this.pull.startX;
1821
+ const dy = t.clientY - this.pull.startY;
1822
+ if (Math.max(Math.abs(dx), Math.abs(dy)) <= SLOP) return;
1823
+ const startY = this.pull.startY;
1824
+ this.pull = null;
1825
+ if (Math.abs(dx) >= Math.abs(dy) || dy < 0 || this.scrolledDown(e)) return;
1826
+ this.drag = { pointerId: -1, startY, lastY: startY, lastT: e.timeStamp, velocity: 0, moving: false, offset: 0 };
1827
+ this.startMoving(this.drag);
1661
1828
  }
1829
+ const drag = this.drag;
1830
+ if (!drag || drag.pointerId !== -1) return;
1831
+ this.follow(drag, t.clientY, e.timeStamp);
1832
+ if (e.cancelable) e.preventDefault();
1833
+ };
1834
+ this.onTouchEnd = (e) => {
1835
+ this.pull = null;
1836
+ const drag = this.drag;
1837
+ if (!drag || drag.pointerId !== -1 || e.touches.length > 0) return;
1838
+ this.finish(drag);
1839
+ };
1840
+ this.onTouchCancel = () => {
1841
+ this.pull = null;
1842
+ if (this.drag?.pointerId === -1) this.abandon();
1662
1843
  };
1663
1844
  this.onPointerCancel = (e) => {
1664
1845
  if (!this.drag || e.pointerId !== this.drag.pointerId) return;
@@ -1693,12 +1874,21 @@ var BottomSheet = class {
1693
1874
  }
1694
1875
  this.opts.host.addEventListener("pointerdown", this.onPointerDown);
1695
1876
  this.opts.host.addEventListener("click", this.onClickCapture, true);
1877
+ this.opts.host.addEventListener("touchstart", this.onTouchStart, { passive: true });
1878
+ this.opts.host.addEventListener("touchmove", this.onTouchMove, { passive: false });
1879
+ this.opts.host.addEventListener("touchend", this.onTouchEnd);
1880
+ this.opts.host.addEventListener("touchcancel", this.onTouchCancel);
1696
1881
  }
1697
1882
  closed() {
1698
1883
  this.opts.host.removeEventListener("pointerdown", this.onPointerDown);
1699
1884
  this.opts.host.removeEventListener("click", this.onClickCapture, true);
1885
+ this.opts.host.removeEventListener("touchstart", this.onTouchStart);
1886
+ this.opts.host.removeEventListener("touchmove", this.onTouchMove);
1887
+ this.opts.host.removeEventListener("touchend", this.onTouchEnd);
1888
+ this.opts.host.removeEventListener("touchcancel", this.onTouchCancel);
1700
1889
  this.stopTracking();
1701
1890
  this.drag = null;
1891
+ this.pull = null;
1702
1892
  const sheet = this.opts.sheet();
1703
1893
  if (sheet) {
1704
1894
  sheet.style.transform = "";
@@ -1714,6 +1904,58 @@ var BottomSheet = class {
1714
1904
  unlockPageScroll();
1715
1905
  }
1716
1906
  }
1907
+ /** Is the content under the finger scrolled away from its top (the sheet's scroller, or one inside it)? */
1908
+ scrolledDown(e) {
1909
+ const sheet = this.opts.sheet();
1910
+ const scroller = this.opts.scroller?.() ?? sheet;
1911
+ if (scroller && scroller.scrollTop > 0) return true;
1912
+ for (const node of e.composedPath()) {
1913
+ if (node === sheet) break;
1914
+ if (node instanceof HTMLElement && node.scrollTop > 0 && node.scrollHeight > node.clientHeight) return true;
1915
+ }
1916
+ return false;
1917
+ }
1918
+ // ── Shared by both ────────────────────────────────────────────────────────
1919
+ /** The press is a drag now: the sheet and backdrop follow the finger without easing. */
1920
+ startMoving(drag) {
1921
+ drag.moving = true;
1922
+ const sheet = this.opts.sheet();
1923
+ if (sheet) sheet.style.transition = "none";
1924
+ const backdrop = this.opts.backdrop?.();
1925
+ if (backdrop) backdrop.style.transition = "none";
1926
+ }
1927
+ follow(drag, y, timeStamp) {
1928
+ const dt = timeStamp - drag.lastT;
1929
+ if (dt > 0) drag.velocity = 0.8 * ((y - drag.lastY) / dt) + 0.2 * drag.velocity;
1930
+ drag.lastY = y;
1931
+ drag.lastT = timeStamp;
1932
+ drag.offset = Math.max(0, y - drag.startY);
1933
+ const sheet = this.opts.sheet();
1934
+ if (sheet) sheet.style.transform = `translateY(${drag.offset}px)`;
1935
+ const backdrop = this.opts.backdrop?.();
1936
+ if (backdrop && sheet) {
1937
+ const height = sheet.getBoundingClientRect().height || window.innerHeight;
1938
+ backdrop.style.opacity = String(Math.max(0, 1 - drag.offset / height));
1939
+ }
1940
+ }
1941
+ /** Let go: close if far or fast enough (and allowed), else spring back. */
1942
+ finish(drag) {
1943
+ this.drag = null;
1944
+ if (!drag.moving) return;
1945
+ this.dragEndedAt = performance.now();
1946
+ const sheet = this.opts.sheet();
1947
+ const height = sheet?.getBoundingClientRect().height || window.innerHeight;
1948
+ const far = drag.offset >= Math.min(CLOSE_DISTANCE, height / 4);
1949
+ const flick = drag.velocity >= FLICK_VELOCITY && drag.offset >= FLICK_MIN;
1950
+ const allowed = !this.opts.canClose || this.opts.canClose();
1951
+ this.release();
1952
+ if ((far || flick) && allowed) {
1953
+ if (sheet) sheet.style.transform = "translateY(100%)";
1954
+ this.opts.close();
1955
+ } else if (sheet) {
1956
+ sheet.style.transform = "";
1957
+ }
1958
+ }
1717
1959
  /** Give up on this press and put the sheet back where it was. */
1718
1960
  abandon() {
1719
1961
  const moving = this.drag?.moving;
@@ -1780,9 +2022,40 @@ function unlockPageScroll() {
1780
2022
  }
1781
2023
 
1782
2024
  // src/ui/components/vio-product-detail.ts
2025
+ var ADDED_TOAST_MS = 8e3;
1783
2026
  var STRIPE_LOGO_SRC = "data:image/svg+xml;utf8," + encodeURIComponent(
1784
2027
  '<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>'
1785
2028
  );
2029
+ var HIGHLIGHT_ICONS = {
2030
+ plane: "M21.5 2.5 2.5 10l7 3m12-10.5-12 10.5m12-10.5L17 21l-7.5-8m0 0V20l3-4M5 17l-2.5 2.5M7.5 18.5 6 20",
2031
+ heart: "M12 20.5s-8.5-5-8.5-11A4.75 4.75 0 0 1 12 6.75 4.75 4.75 0 0 1 20.5 9.5c0 6-8.5 11-8.5 11Z",
2032
+ shirt: "M8.5 3.5 3 6l1.75 4.25L7 9.5v11h10v-11l2.25.75L21 6l-5.5-2.5a3.5 3.5 0 0 1-7 0Z",
2033
+ truck: "M2.5 5.5h11v10h-11zM13.5 9.5h4l3 3.5v2.5h-7M6.5 18.5a2 2 0 1 0 0-.01M17 18.5a2 2 0 1 0 0-.01",
2034
+ return: "M4 9h11.5a4.5 4.5 0 0 1 0 9H8M4 9l4-4M4 9l4 4",
2035
+ leaf: "M5 19C5 10 10 4.5 20 4.5c0 10-5.5 15-14 15M5 19l7-7",
2036
+ star: "m12 3.5 2.6 5.4 5.9.8-4.3 4.1 1 5.9L12 17l-5.2 2.7 1-5.9-4.3-4.1 5.9-.8Z",
2037
+ shield: "M12 3 4.5 6v5.5c0 4.5 3.2 8.2 7.5 9.5 4.3-1.3 7.5-5 7.5-9.5V6Z",
2038
+ mailbox: "M3 19v-8a4 4 0 0 1 8 0v8m-8 0h18v-7a4 4 0 0 0-4-4H7m8 0V4h3M6 12h2",
2039
+ home: "M3 11 12 4l9 7M5.5 9.5V20h13V9.5M10 20v-5h4v5",
2040
+ pin: "M12 21s-6.5-6-6.5-11a6.5 6.5 0 0 1 13 0c0 5-6.5 11-6.5 11Zm0-8.5a2.5 2.5 0 1 0 0-5 2.5 2.5 0 0 0 0 5Z",
2041
+ clock: "M12 21a9 9 0 1 0 0-18 9 9 0 0 0 0 18Zm0-13.5V12l3 2"
2042
+ };
2043
+ function rateIcon(method) {
2044
+ const m = method.toLowerCase();
2045
+ if (/postkasse|mailbox|brevkasse/.test(m)) return "mailbox";
2046
+ if (/hente|pakkeboks|automat|postkontor|pickup|utlever/.test(m)) return "pin";
2047
+ if (/dør|hjem|home|door/.test(m)) return "home";
2048
+ return "truck";
2049
+ }
2050
+ function carrierLogo(logos, carrier) {
2051
+ if (!logos || !carrier) return "";
2052
+ const want = carrier.trim().toLowerCase();
2053
+ for (const [name, url] of Object.entries(logos)) if (name.trim().toLowerCase() === want && url) return url;
2054
+ return "";
2055
+ }
2056
+ function lineIcon(name, cls = "line-icon") {
2057
+ return html`<svg class=${cls} viewBox="0 0 24 24" aria-hidden="true"><path d=${HIGHLIGHT_ICONS[name] ?? ""}></path></svg>`;
2058
+ }
1786
2059
  var VioProductDetail = class extends LitElement {
1787
2060
  constructor() {
1788
2061
  super(...arguments);
@@ -1798,6 +2071,12 @@ var VioProductDetail = class extends LitElement {
1798
2071
  this.vioCampaign = "";
1799
2072
  this.vioContentId = "";
1800
2073
  this.vioContentTitle = "";
2074
+ this.noAddedToast = false;
2075
+ this.gallery = "";
2076
+ this.descriptionMode = "";
2077
+ this.highlights = [];
2078
+ this.deliveryTime = "";
2079
+ this.carrierLogos = {};
1801
2080
  this.slotRegistration = new SlotRegistration(this, "detail");
1802
2081
  /** What the click that opened the detail carried ('' when nothing), and for which product. */
1803
2082
  this.eventSlot = "";
@@ -1808,18 +2087,31 @@ var VioProductDetail = class extends LitElement {
1808
2087
  this.product = null;
1809
2088
  this.sellerName = null;
1810
2089
  this.sellerSettled = false;
2090
+ this.shippingFrom = null;
2091
+ this.shippingRates = [];
1811
2092
  this.isLoading = false;
1812
2093
  this.fetchError = "";
1813
2094
  this.selectedOptions = {};
2095
+ this.selectionPrompt = "";
1814
2096
  this.quantity = 1;
1815
2097
  this.activeImageIndex = 0;
1816
2098
  this.thumbsOverflow = false;
1817
2099
  this.thumbsAtStart = true;
1818
2100
  this.thumbsAtEnd = false;
2101
+ this.descriptionOpen = false;
2102
+ this.descriptionClipped = false;
2103
+ this.galleryMode = "thumbs";
2104
+ this.resolvedDescriptionMode = "preview";
1819
2105
  this.logoSquare = false;
1820
2106
  this.adding = false;
2107
+ this.addedToast = null;
2108
+ this.addedToastTimer = null;
1821
2109
  this.applePayOk = false;
1822
2110
  this.availableMethods = null;
2111
+ this.methodsLoading = false;
2112
+ this.vippsExpress = false;
2113
+ /** Whose methods `availableMethods` are — another brand's are not shown while its own load. */
2114
+ this.methodsSponsorId = 0;
1823
2115
  this.vippsOfficial = false;
1824
2116
  /** Race guard: only the latest fetch may write state (two quick taps on
1825
2117
  * different cards would otherwise let the slow response win). */
@@ -1873,6 +2165,15 @@ var VioProductDetail = class extends LitElement {
1873
2165
  this.show();
1874
2166
  void this.fetchProduct();
1875
2167
  };
2168
+ /** The photo in view is the active one (dots, Apple Pay sheet, events). */
2169
+ this.onCarouselScroll = () => {
2170
+ const track = this.carouselTrack();
2171
+ const step = this.carouselStep(track);
2172
+ if (!track || !step) return;
2173
+ const last = track.children.length - 1;
2174
+ const idx = Math.min(last, Math.max(0, Math.round(track.scrollLeft / step)));
2175
+ if (idx !== this.activeImageIndex) this.activeImageIndex = idx;
2176
+ };
1876
2177
  this.onThumbsScroll = () => {
1877
2178
  const row = this.thumbRow();
1878
2179
  if (!row) return;
@@ -2187,6 +2488,103 @@ var VioProductDetail = class extends LitElement {
2187
2488
  .thumbs.overflows .thumbs-arrow { display: flex; }
2188
2489
  }
2189
2490
 
2491
+ /* Carousel (gallery="carousel" / theme detailGallery): the same box as the
2492
+ main image — proportion, fit, background, inset — one per photo, in a
2493
+ row that snaps. --vio-detail-carousel-peek shows the edge of the next. */
2494
+ .carousel {
2495
+ position: relative;
2496
+ margin-inline: calc(
2497
+ var(--vio-detail-image-inset, var(--vio-detail-padding-x, var(--vio-space-lg, 24px))) -
2498
+ var(--vio-detail-padding-x, var(--vio-space-lg, 24px))
2499
+ );
2500
+ }
2501
+ .carousel-track {
2502
+ display: flex;
2503
+ gap: var(--vio-detail-carousel-gap, 0px);
2504
+ overflow-x: auto;
2505
+ scroll-snap-type: x mandatory;
2506
+ scroll-behavior: smooth;
2507
+ scrollbar-width: none;
2508
+ -webkit-overflow-scrolling: touch;
2509
+ overscroll-behavior-x: contain;
2510
+ /* Sideways is the carousel's, up and down the panel's; no double-tap
2511
+ zoom delay. The sheet only drags from its top bar, never from here. */
2512
+ touch-action: pan-x pan-y;
2513
+ border-radius: var(--vio-detail-image-radius, 0);
2514
+ }
2515
+ .carousel-track::-webkit-scrollbar { display: none; }
2516
+ .carousel-slide {
2517
+ flex: 0 0 calc(100% - var(--vio-detail-carousel-peek, 0px));
2518
+ aspect-ratio: var(--vio-detail-image-aspect, 4 / 5);
2519
+ max-height: var(--vio-detail-image-max-height, 85vh);
2520
+ background: var(--vio-detail-image-bg, var(--vio-color-surface-muted, #f2f2f2));
2521
+ scroll-snap-align: start;
2522
+ scroll-snap-stop: always;
2523
+ overflow: hidden;
2524
+ }
2525
+ .carousel-slide img {
2526
+ display: block;
2527
+ /* A press on a photo is a swipe: no image drag, no long-press menu, no selection. */
2528
+ pointer-events: none;
2529
+ user-select: none;
2530
+ -webkit-user-select: none;
2531
+ -webkit-user-drag: none;
2532
+ -webkit-touch-callout: none;
2533
+ width: 100%;
2534
+ height: 100%;
2535
+ object-fit: var(--vio-detail-image-fit, cover);
2536
+ object-position: var(--vio-detail-image-position, center);
2537
+ transform: scale(var(--vio-detail-image-zoom, 1));
2538
+ }
2539
+ /* One dot per photo over the bottom of the image: there is more than one,
2540
+ and which one this is. White with a shadow reads on light and dark photos. */
2541
+ .carousel-dots {
2542
+ position: absolute;
2543
+ left: 0;
2544
+ right: 0;
2545
+ bottom: 12px;
2546
+ display: flex;
2547
+ justify-content: center;
2548
+ gap: 6px;
2549
+ pointer-events: none;
2550
+ }
2551
+ .carousel-dot {
2552
+ width: 6px;
2553
+ height: 6px;
2554
+ border-radius: 50%;
2555
+ background: var(--vio-detail-dot-color, rgba(255, 255, 255, 0.6));
2556
+ box-shadow: 0 0 2px rgba(0, 0, 0, 0.35);
2557
+ transition: background 0.2s ease, transform 0.2s ease;
2558
+ }
2559
+ .carousel-dot.active {
2560
+ background: var(--vio-detail-dot-active-color, #fff);
2561
+ transform: scale(1.35);
2562
+ }
2563
+ .carousel-arrow {
2564
+ display: none;
2565
+ position: absolute;
2566
+ top: 50%;
2567
+ transform: translateY(-50%);
2568
+ width: 36px;
2569
+ height: 36px;
2570
+ border-radius: 50%;
2571
+ border: 1px solid var(--vio-color-border, #e5e5e5);
2572
+ background: var(--vio-color-surface, #fff);
2573
+ color: var(--vio-color-text, #0a0a0a);
2574
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.12);
2575
+ cursor: pointer;
2576
+ align-items: center;
2577
+ justify-content: center;
2578
+ padding: 0;
2579
+ font: 18px/1 sans-serif;
2580
+ }
2581
+ .carousel-arrow.prev { left: 12px; }
2582
+ .carousel-arrow.next { right: 12px; }
2583
+ .carousel-arrow:disabled { visibility: hidden; }
2584
+ @media (hover: hover) and (pointer: fine) {
2585
+ .carousel-arrow { display: flex; }
2586
+ }
2587
+
2190
2588
  /* Info column */
2191
2589
  .info { display: flex; flex-direction: column; gap: var(--vio-detail-info-gap, 20px); min-width: 0; }
2192
2590
  .brand {
@@ -2204,11 +2602,88 @@ var VioProductDetail = class extends LitElement {
2204
2602
  margin: 0;
2205
2603
  letter-spacing: -0.01em;
2206
2604
  }
2605
+ /* The brand's promises: one row, an icon over a title and a line of text,
2606
+ centred like a shop's own trust strip. */
2607
+ .highlights {
2608
+ list-style: none;
2609
+ /* --vio-detail-highlights-display: none hides the strip (theme detailHighlightsDisplay). */
2610
+ margin: 0;
2611
+ padding: var(--vio-detail-highlights-padding, 16px 0 2px);
2612
+ display: var(--vio-detail-highlights-display, grid);
2613
+ grid-auto-flow: column;
2614
+ grid-auto-columns: 1fr;
2615
+ gap: 10px;
2616
+ border-top: 1px solid var(--vio-color-border, #e5e5e5);
2617
+ }
2618
+ /* Like a shop's trust strip: a thin icon, the promise in regular weight,
2619
+ a smaller line under it — nothing bold. */
2620
+ .hl { display: flex; flex-direction: column; align-items: center; text-align: center; gap: 2px; min-width: 0; }
2621
+ .hl-icon {
2622
+ width: var(--vio-detail-highlight-icon-size, 22px);
2623
+ height: var(--vio-detail-highlight-icon-size, 22px);
2624
+ margin-bottom: 8px;
2625
+ fill: none;
2626
+ stroke: currentColor;
2627
+ stroke-width: 1.2;
2628
+ stroke-linecap: round;
2629
+ stroke-linejoin: round;
2630
+ object-fit: contain;
2631
+ }
2632
+ .hl-title { font-size: 12px; font-weight: 400; line-height: 1.3; letter-spacing: 0.01em; color: var(--vio-color-text, #0a0a0a); }
2633
+ .hl-text { font-size: 10.5px; line-height: 1.35; letter-spacing: 0.01em; color: var(--vio-color-text-secondary, #666); }
2634
+ /* «Frakt & retur»: same fold as «Beskrivelse», with a small list of rates.
2635
+ --vio-detail-shipping-returns-display: none hides it (theme detailShippingReturnsDisplay). */
2636
+ .shipping-returns { display: var(--vio-detail-shipping-returns-display, block); }
2637
+ .description.shipping-returns-body { white-space: normal; }
2638
+ .shipping-returns-body p { margin: 0 0 10px; }
2639
+ .sr-title {
2640
+ font-size: 11px;
2641
+ text-transform: uppercase;
2642
+ letter-spacing: var(--vio-tracking-label, 0.1em);
2643
+ color: var(--vio-color-text-secondary, #666);
2644
+ font-weight: 600;
2645
+ margin: 4px 0 6px;
2646
+ }
2647
+ .sr-rates { list-style: none; margin: 0 0 10px; padding: 0; }
2648
+ /* One row per carrier: «Helthjem» over «Til postkasse/dørmatte», price right. */
2649
+ .sr-rates li {
2650
+ display: flex;
2651
+ align-items: center;
2652
+ gap: 12px;
2653
+ padding: 8px 0;
2654
+ border-bottom: 1px solid var(--vio-color-border, #eee);
2655
+ }
2656
+ .sr-rates li:last-child { border-bottom: none; }
2657
+ .sr-rate { flex: 1; display: flex; flex-direction: column; min-width: 0; line-height: 1.35; }
2658
+ .line-icon { fill: none; stroke: currentColor; stroke-width: 1.3; stroke-linecap: round; stroke-linejoin: round; }
2659
+ .sr-icon { flex: 0 0 auto; width: 20px; height: 20px; color: var(--vio-color-text, #0a0a0a); }
2660
+ .sr-line { display: flex; align-items: center; gap: 12px; }
2661
+ /* A carrier's logo in a fixed box, so rows line up whatever its shape. */
2662
+ .sr-logo { flex: 0 0 auto; width: var(--vio-detail-carrier-logo-width, 56px); height: 24px; object-fit: contain; object-position: left center; }
2663
+ .sr-carrier { font-weight: 600; color: var(--vio-color-text, #0a0a0a); }
2664
+ .sr-method { font-size: 13px; }
2665
+ .sr-time { margin: 0 0 14px; }
2666
+ .sr-price { font-weight: 600; color: var(--vio-color-text, #0a0a0a); white-space: nowrap; }
2667
+ .sr-links { display: flex; gap: 16px; padding-left: 32px; }
2668
+ .sr-links a { color: inherit; }
2669
+ /* Name and price read as one block (stock on the name's line, right);
2670
+ the sizes come after a wider gap, then shipping/returns by the buttons. */
2671
+ .headline { display: flex; flex-direction: column; gap: var(--vio-detail-headline-gap, 8px); }
2672
+ .title-row {
2673
+ display: flex;
2674
+ align-items: baseline;
2675
+ justify-content: space-between;
2676
+ gap: 16px;
2677
+ }
2678
+ .title-row .name { min-width: 0; }
2679
+ .title-row .stock { flex: 0 0 auto; white-space: nowrap; }
2207
2680
  .price-row {
2208
2681
  display: flex;
2209
2682
  align-items: baseline;
2210
2683
  gap: 16px;
2211
2684
  }
2685
+ /* The sizes follow the price closely: they are the next thing to do. */
2686
+ .headline + .purchase { margin-top: calc(var(--vio-detail-sizes-gap, 2px) - var(--vio-detail-info-gap, 20px)); }
2212
2687
  .price {
2213
2688
  font-size: var(--vio-detail-price-size, 24px);
2214
2689
  font-weight: var(--vio-detail-price-weight, 700);
@@ -2218,6 +2693,11 @@ var VioProductDetail = class extends LitElement {
2218
2693
  color: var(--vio-color-text-tertiary, #999);
2219
2694
  text-decoration: line-through;
2220
2695
  }
2696
+ .delivery-note {
2697
+ margin-top: calc(var(--vio-detail-info-gap, 20px) * -0.6);
2698
+ font-size: 13px;
2699
+ color: var(--vio-color-text-secondary, #666);
2700
+ }
2221
2701
  .stock {
2222
2702
  font-size: 12px;
2223
2703
  text-transform: uppercase;
@@ -2229,8 +2709,15 @@ var VioProductDetail = class extends LitElement {
2229
2709
  .stock.out-of-stock { color: var(--vio-color-text-tertiary, #999); }
2230
2710
 
2231
2711
  /* Option pickers */
2232
- .option-group { display: flex; flex-direction: column; gap: 6px; }
2712
+ .option-group { display: flex; flex-direction: column; gap: 8px; }
2713
+ /* The prompt sits on the label's own line: tapping a buy button before
2714
+ choosing must not push the pills down under the shopper's finger. */
2233
2715
  .option-label {
2716
+ display: flex;
2717
+ align-items: baseline;
2718
+ justify-content: space-between;
2719
+ gap: 12px;
2720
+ min-height: 1.4em;
2234
2721
  font-size: 11px;
2235
2722
  text-transform: uppercase;
2236
2723
  letter-spacing: var(--vio-tracking-label, 0.1em);
@@ -2276,6 +2763,19 @@ var VioProductDetail = class extends LitElement {
2276
2763
  background: repeating-linear-gradient(-45deg, #fafafa, #fafafa 5px, #f0f0f0 5px, #f0f0f0 10px);
2277
2764
  }
2278
2765
  .option-pill.unavailable:hover { border-color: var(--vio-color-border, #e5e5e5); }
2766
+ /* The value chosen, beside the option's name: «Størrelse: M». */
2767
+ .option-chosen { color: var(--vio-color-text, #0a0a0a); font-weight: 700; }
2768
+ /* A buy button was tapped before choosing: say what is missing. */
2769
+ .option-prompt {
2770
+ text-transform: none;
2771
+ letter-spacing: normal;
2772
+ font-size: 13px;
2773
+ font-weight: 600;
2774
+ color: var(--vio-color-accent, #c14a3b);
2775
+ }
2776
+ .option-group.needs-choice .option-pill:not(.unavailable) {
2777
+ border-color: var(--vio-color-accent, #c14a3b);
2778
+ }
2279
2779
 
2280
2780
  /* The buy zone: Vipps first, then quantity + add to cart, then the other
2281
2781
  ways to pay. --vio-detail-buy-position: sticky pins it to the bottom of
@@ -2292,6 +2792,37 @@ var VioProductDetail = class extends LitElement {
2292
2792
  box-shadow: var(--vio-detail-buy-shadow, none);
2293
2793
  }
2294
2794
  .buy > * { margin-top: 0 !important; }
2795
+ /* The buy zone and the description, apart from the name, price and stock:
2796
+ a pinned buy zone can slide up over what is in this box (the
2797
+ description) but never above it — never over the name, the price or
2798
+ the stock line (Mote & Livsstil, 2026-10-28). */
2799
+ .purchase { display: flex; flex-direction: column; gap: var(--vio-detail-info-gap, 20px); }
2800
+ /* The pickers live in the buy zone: when it is pinned to the bottom of the
2801
+ panel (desktop, or a host's sticky), it can never cover them — they
2802
+ travel with the buttons they gate (Mote & Livsstil, 2026-10-28). */
2803
+ .buy-options {
2804
+ display: flex;
2805
+ flex-direction: column;
2806
+ gap: var(--vio-detail-info-gap, 20px);
2807
+ padding-bottom: 4px;
2808
+ }
2809
+ /* Where the express buttons go while the brand's methods load: neutral,
2810
+ the height of one button, no method named. */
2811
+ .buy-pending {
2812
+ height: var(--vio-detail-button-height, 52px);
2813
+ border-radius: var(--vio-detail-button-radius, 0);
2814
+ background: linear-gradient(
2815
+ 100deg,
2816
+ var(--vio-color-surface-muted, #f1f1f1) 30%,
2817
+ rgba(255, 255, 255, 0.65) 50%,
2818
+ var(--vio-color-surface-muted, #f1f1f1) 70%
2819
+ );
2820
+ background-size: 200% 100%;
2821
+ animation: vio-skeleton-shimmer 1.4s ease-in-out infinite;
2822
+ }
2823
+ @media (prefers-reduced-motion: reduce) {
2824
+ .buy-pending { animation: none; background: var(--vio-color-surface-muted, #f1f1f1); }
2825
+ }
2295
2826
  /* Under the buttons: who the shopper is buying from. */
2296
2827
  .seller-note {
2297
2828
  margin: 2px 0 0;
@@ -2424,6 +2955,14 @@ var VioProductDetail = class extends LitElement {
2424
2955
  }
2425
2956
  .buy-vipps:hover:not(:disabled) { background: #ec4f1c; }
2426
2957
  .buy-vipps:disabled { opacity: 0.5; cursor: not-allowed; }
2958
+ /* Under the Express button: what happens in the app. */
2959
+ .vipps-note {
2960
+ margin: 0;
2961
+ font-size: 12px;
2962
+ line-height: 1.4;
2963
+ color: var(--vio-color-text-secondary, #666);
2964
+ text-align: center;
2965
+ }
2427
2966
  /* Vipps' own button (web component) — same slot, their drawing. */
2428
2967
  .buy-vipps-official { display: block; width: 100%; margin-top: 10px; cursor: pointer; }
2429
2968
  /* Apple Pay express button — black, per Apple's button guidelines. Only
@@ -2475,6 +3014,61 @@ var VioProductDetail = class extends LitElement {
2475
3014
  transition: transform 0.15s ease;
2476
3015
  }
2477
3016
  .description-block[open] > summary::after { transform: rotate(-135deg) translateY(-2px); }
3017
+ /* «Beskrivelse» with a preview: the same heading as the folds, then the
3018
+ first lines fading out, then «Les mer». */
3019
+ .desc-head {
3020
+ all: unset;
3021
+ box-sizing: border-box;
3022
+ width: 100%;
3023
+ cursor: pointer;
3024
+ display: flex;
3025
+ align-items: center;
3026
+ justify-content: space-between;
3027
+ padding: 14px 0 6px;
3028
+ font-size: 11px;
3029
+ text-transform: uppercase;
3030
+ letter-spacing: var(--vio-tracking-label, 0.1em);
3031
+ color: var(--vio-color-text-secondary, #666);
3032
+ font-weight: 600;
3033
+ font-family: inherit;
3034
+ }
3035
+ .description-preview.clipped .desc-head::after {
3036
+ content: '';
3037
+ width: 8px;
3038
+ height: 8px;
3039
+ border-right: 1.5px solid currentColor;
3040
+ border-bottom: 1.5px solid currentColor;
3041
+ transform: rotate(45deg) translateY(-2px);
3042
+ transition: transform 0.15s ease;
3043
+ }
3044
+ .description-preview.open .desc-head::after { transform: rotate(-135deg) translateY(-2px); }
3045
+ .description-preview .description { padding-bottom: 6px; }
3046
+ .description-preview.folded .desc-head, .description-preview.open:not(.clipped) .desc-head { padding-bottom: 14px; }
3047
+ .description-preview.folded.open .desc-head { padding-bottom: 6px; }
3048
+ .description-preview.folded .description { padding-bottom: 14px; }
3049
+ .description-preview:not(.clipped) .description { padding-bottom: 14px; }
3050
+ .description.collapsed {
3051
+ display: -webkit-box;
3052
+ -webkit-box-orient: vertical;
3053
+ -webkit-line-clamp: var(--vio-detail-description-preview-lines, 3);
3054
+ line-clamp: var(--vio-detail-description-preview-lines, 3);
3055
+ overflow: hidden;
3056
+ cursor: pointer;
3057
+ -webkit-mask-image: linear-gradient(to bottom, #000 45%, transparent);
3058
+ mask-image: linear-gradient(to bottom, #000 45%, transparent);
3059
+ }
3060
+ .desc-more {
3061
+ all: unset;
3062
+ cursor: pointer;
3063
+ display: inline-block;
3064
+ margin: 0 0 14px;
3065
+ font-size: 13px;
3066
+ font-weight: 600;
3067
+ color: var(--vio-color-text, #0a0a0a);
3068
+ text-decoration: underline;
3069
+ text-underline-offset: 3px;
3070
+ font-family: inherit;
3071
+ }
2478
3072
  .description {
2479
3073
  font-size: 14px;
2480
3074
  line-height: 1.6;
@@ -2482,15 +3076,13 @@ var VioProductDetail = class extends LitElement {
2482
3076
  padding-bottom: 14px;
2483
3077
  white-space: pre-line;
2484
3078
  }
2485
- /* Desktop: the description above, the buy zone at the bottom of the panel
2486
- (sticky). Phones keep the order as it is: buy, then the description. */
3079
+ /* Same order on every screen: buy, then the description at the very
3080
+ bottom (folded). Hosts that want the buy zone pinned can still set
3081
+ --vio-detail-buy-position: sticky. */
2487
3082
  @media (min-width: 601px) {
2488
- .description-block { order: 1; }
2489
3083
  .buy {
2490
- order: 2;
2491
- position: var(--vio-detail-buy-position, sticky);
3084
+ position: var(--vio-detail-buy-position, static);
2492
3085
  padding: var(--vio-detail-buy-padding, 12px 0 16px);
2493
- box-shadow: var(--vio-detail-buy-shadow, 0 -1px 0 var(--vio-color-border, #e5e5e5));
2494
3086
  }
2495
3087
  }
2496
3088
 
@@ -2530,6 +3122,49 @@ var VioProductDetail = class extends LitElement {
2530
3122
  background: var(--vio-color-surface-muted, #f1f1f1);
2531
3123
  }
2532
3124
  }
3125
+ /* «Lagt i handlekurven»: above the page, below where a thumb reaches. */
3126
+ .added-toast {
3127
+ position: fixed;
3128
+ left: 50%;
3129
+ bottom: max(16px, env(safe-area-inset-bottom));
3130
+ transform: translateX(-50%);
3131
+ z-index: 1102;
3132
+ width: min(420px, calc(100vw - 32px));
3133
+ box-sizing: border-box;
3134
+ padding: 14px 16px;
3135
+ background: var(--vio-color-surface, #fff);
3136
+ color: var(--vio-color-text, #0a0a0a);
3137
+ border: 1px solid var(--vio-color-border, #e5e5e5);
3138
+ border-radius: var(--vio-radius-lg, 8px);
3139
+ box-shadow: 0 8px 24px rgba(0, 0, 0, 0.16);
3140
+ font-family: var(--vio-font-sans, -apple-system, sans-serif);
3141
+ display: flex;
3142
+ flex-direction: column;
3143
+ gap: 12px;
3144
+ }
3145
+ .added-toast-text { font-size: 14px; font-weight: 600; }
3146
+ .added-toast-text::before { content: '✓ '; color: #2e7d32; }
3147
+ .added-toast-actions { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; }
3148
+ .added-toast-actions button {
3149
+ min-height: 44px;
3150
+ padding: 0 12px;
3151
+ font-family: inherit;
3152
+ font-size: 13px;
3153
+ font-weight: 600;
3154
+ cursor: pointer;
3155
+ border-radius: var(--vio-detail-button-radius, 0);
3156
+ }
3157
+ .added-toast-checkout {
3158
+ background: var(--vio-color-text, #0a0a0a);
3159
+ color: var(--vio-color-text-on-primary, #fff);
3160
+ border: 1px solid var(--vio-color-text, #0a0a0a);
3161
+ }
3162
+ .added-toast-continue {
3163
+ background: transparent;
3164
+ color: var(--vio-color-text, #0a0a0a);
3165
+ border: 1px solid var(--vio-color-border, #e5e5e5);
3166
+ }
3167
+
2533
3168
  .error {
2534
3169
  padding: 80px 24px;
2535
3170
  text-align: center;
@@ -2545,6 +3180,8 @@ var VioProductDetail = class extends LitElement {
2545
3180
  document.addEventListener("vio:product-click", this.boundProductClick);
2546
3181
  }
2547
3182
  disconnectedCallback() {
3183
+ if (this.addedToastTimer) clearTimeout(this.addedToastTimer);
3184
+ this.addedToastTimer = null;
2548
3185
  document.removeEventListener("vio:product-click", this.boundProductClick);
2549
3186
  this.slotRegistration.clear();
2550
3187
  this.bottomSheet.disconnect();
@@ -2555,16 +3192,63 @@ var VioProductDetail = class extends LitElement {
2555
3192
  this.dialogFocus.update(this.open);
2556
3193
  this.bottomSheet.update(this.open);
2557
3194
  }
2558
- if (changed.has("product") || changed.has("open")) this.measureThumbs();
3195
+ if (changed.has("product") || changed.has("open")) {
3196
+ this.measureThumbs();
3197
+ this.measureDescription();
3198
+ }
2559
3199
  if ((changed.has("quantity") || changed.has("selectedOptions")) && this.product && this.applePayAskedFor !== null) {
2560
3200
  if (this.unitPrice * this.quantity !== this.applePayAskedFor) this.prepareApplePaySheet();
2561
3201
  }
2562
- if (changed.has("activeImageIndex")) this.revealActiveThumb();
3202
+ if (changed.has("activeImageIndex")) {
3203
+ this.revealActiveThumb();
3204
+ this.revealActiveSlide();
3205
+ }
3206
+ if (changed.has("selectedOptions") && this.galleryMode === "carousel") {
3207
+ if (this.activeImageIndex !== 0) this.activeImageIndex = 0;
3208
+ else this.revealActiveSlide();
3209
+ }
2563
3210
  this.measureLogo();
2564
3211
  if (changed.has("vioSlot") || changed.has("vioSlotName") || changed.has("vioScope") || changed.has("vioScopeName") || changed.has("vioCampaign")) {
2565
3212
  this.slotRegistration.sync(this.slotAttributes());
2566
3213
  }
2567
3214
  }
3215
+ willUpdate(changed) {
3216
+ if (changed.has("gallery") || changed.has("open") && this.open) this.galleryMode = this.resolveGalleryMode();
3217
+ if (changed.has("descriptionMode") || changed.has("open") && this.open) {
3218
+ const value = this.themeChoice(this.descriptionMode, "--vio-detail-description-mode");
3219
+ this.resolvedDescriptionMode = value === "folded" || value === "open" ? value : "preview";
3220
+ }
3221
+ }
3222
+ /** An option as the element says it, else as the theme's custom property says it (lower case, unquoted). */
3223
+ themeChoice(own, cssVar) {
3224
+ const fromTheme = typeof getComputedStyle === "function" ? getComputedStyle(this).getPropertyValue(cssVar) : "";
3225
+ return (own || fromTheme).trim().replace(/^["']|["']$/g, "").toLowerCase();
3226
+ }
3227
+ /** The attribute wins; else the theme's `--vio-detail-gallery`; else thumbnails. */
3228
+ resolveGalleryMode() {
3229
+ return this.themeChoice(this.gallery, "--vio-detail-gallery") === "carousel" ? "carousel" : "thumbs";
3230
+ }
3231
+ carouselTrack() {
3232
+ return this.renderRoot.querySelector(".carousel-track");
3233
+ }
3234
+ /** One photo's width plus the gap (with a peek a photo is narrower than the row). */
3235
+ carouselStep(track) {
3236
+ const slide = track?.firstElementChild;
3237
+ if (!track || !slide || slide.offsetWidth === 0) return 0;
3238
+ return slide.offsetWidth + (parseFloat(getComputedStyle(track).columnGap) || 0);
3239
+ }
3240
+ /** Bring the active photo into view — only when it is not already the one a swipe is on. */
3241
+ revealActiveSlide() {
3242
+ const track = this.carouselTrack();
3243
+ const step = this.carouselStep(track);
3244
+ if (!track || !step) return;
3245
+ const target = Math.min(this.activeImageIndex * step, track.scrollWidth - track.clientWidth);
3246
+ if (Math.abs(track.scrollLeft - target) > step / 2) track.scrollTo({ left: target });
3247
+ }
3248
+ stepCarousel(direction) {
3249
+ const last = this.allImages().length - 1;
3250
+ this.activeImageIndex = Math.min(last, Math.max(0, this.activeImageIndex + direction));
3251
+ }
2568
3252
  thumbRow() {
2569
3253
  return this.renderRoot.querySelector(".gallery-thumbs");
2570
3254
  }
@@ -2593,6 +3277,7 @@ var VioProductDetail = class extends LitElement {
2593
3277
  }
2594
3278
  show() {
2595
3279
  this.open = true;
3280
+ this.hideAddedToast();
2596
3281
  }
2597
3282
  close() {
2598
3283
  this.open = false;
@@ -2606,9 +3291,12 @@ var VioProductDetail = class extends LitElement {
2606
3291
  this.product = null;
2607
3292
  this.sellerName = null;
2608
3293
  this.sellerSettled = false;
3294
+ this.shippingFrom = null;
2609
3295
  this.selectedOptions = {};
3296
+ this.selectionPrompt = "";
2610
3297
  this.quantity = 1;
2611
3298
  this.activeImageIndex = 0;
3299
+ this.descriptionOpen = false;
2612
3300
  this.fetchError = "";
2613
3301
  }
2614
3302
  }, 400);
@@ -2624,6 +3312,7 @@ var VioProductDetail = class extends LitElement {
2624
3312
  this.fetchError = "";
2625
3313
  this.sellerName = null;
2626
3314
  this.sellerSettled = false;
3315
+ this.shippingFrom = null;
2627
3316
  let products = null;
2628
3317
  let lastError = null;
2629
3318
  for (let attempt = 0; attempt < 4; attempt++) {
@@ -2653,6 +3342,7 @@ var VioProductDetail = class extends LitElement {
2653
3342
  this.optMapCache = /* @__PURE__ */ new WeakMap();
2654
3343
  this.product = products?.[0] ?? null;
2655
3344
  if (this.product) void this.loadSeller(productIdNum, seq);
3345
+ if (this.product) void this.loadShippingFrom(productIdNum, seq);
2656
3346
  if (this.product) {
2657
3347
  try {
2658
3348
  const price = Number(
@@ -2685,9 +3375,10 @@ var VioProductDetail = class extends LitElement {
2685
3375
  const initial = {};
2686
3376
  for (const opt of this.product.options ?? []) {
2687
3377
  const values = normalizeValues(opt.values);
2688
- if (values.length > 0) initial[opt.name] = values[0];
3378
+ if (values.length === 1) initial[opt.name] = values[0];
2689
3379
  }
2690
3380
  this.selectedOptions = initial;
3381
+ this.selectionPrompt = "";
2691
3382
  this.prepareApplePaySheet();
2692
3383
  void this.loadAvailablePaymentMethods();
2693
3384
  }
@@ -2697,22 +3388,41 @@ var VioProductDetail = class extends LitElement {
2697
3388
  async loadAvailablePaymentMethods() {
2698
3389
  const spId = this.sponsorId || (typeof Vio.campaignForSlot === "function" ? Number(Vio.campaignForSlot(this.activeSlot || null, { campaignId: this.activeCampaignHint })?.primarySponsor?.id) : 0) || globalThis.__VIO_SPONSOR_ID__;
2699
3390
  if (!spId) return;
3391
+ if (spId !== this.methodsSponsorId) {
3392
+ this.availableMethods = null;
3393
+ this.vippsExpress = false;
3394
+ }
3395
+ this.methodsSponsorId = spId;
3396
+ this.methodsLoading = true;
2700
3397
  try {
2701
3398
  const methods = await Vio.checkout.getAvailablePaymentMethods(spId);
2702
3399
  if (Array.isArray(methods)) {
2703
3400
  this.availableMethods = methods.map((m) => m.name);
3401
+ const vipps = methods.find(
3402
+ (m) => String(m?.name ?? "").toLowerCase() === "vipps"
3403
+ );
3404
+ this.vippsExpress = !!vipps && vipps.config?.find((c) => c?.name === "express")?.value !== "false";
2704
3405
  } else {
2705
3406
  this.availableMethods = null;
3407
+ this.vippsExpress = false;
2706
3408
  }
2707
3409
  } catch (err) {
2708
3410
  if (typeof console !== "undefined") {
2709
3411
  console.warn("[VioProductDetail] Failed to load payment methods:", err);
2710
3412
  }
2711
3413
  this.availableMethods = null;
3414
+ this.vippsExpress = false;
3415
+ } finally {
3416
+ this.methodsLoading = false;
2712
3417
  }
2713
3418
  }
3419
+ /** The methods are still on their way: a placeholder stands where the buttons go. */
3420
+ get methodsPending() {
3421
+ return this.methodsLoading && this.availableMethods === null;
3422
+ }
2714
3423
  /** Backend names arrive as e.g. "Apple Pay" — compare letters only. */
2715
3424
  methodEnabled(...names) {
3425
+ if (this.methodsPending) return false;
2716
3426
  return isMethodEnabled(this.availableMethods, ...names);
2717
3427
  }
2718
3428
  /* eslint-disable @typescript-eslint/no-explicit-any */
@@ -2901,26 +3611,92 @@ var VioProductDetail = class extends LitElement {
2901
3611
  const price = v?.price ?? this.product?.price;
2902
3612
  return price?.amount_incl_taxes ?? price?.amount ?? 0;
2903
3613
  }
3614
+ /**
3615
+ * There is something to pay for — whether the express buttons show. With
3616
+ * every option chosen, the chosen variant's price; before that, the
3617
+ * product's or any variant's: the first variant (what `unitPrice` falls
3618
+ * back to) at 0 must not hide Vipps for a size that costs 699.
3619
+ */
3620
+ get hasPrice() {
3621
+ if (!this.missingOption()) return this.unitPrice > 0;
3622
+ const priceOf = (p) => Number(p?.amount_incl_taxes ?? p?.amount ?? 0);
3623
+ return priceOf(this.product?.price) > 0 || (this.product?.variants ?? []).some((v) => priceOf(v.price) > 0);
3624
+ }
2904
3625
  get compareAt() {
2905
3626
  const v = this.selectedVariant();
2906
3627
  const price = v?.price ?? this.product?.price;
2907
3628
  return price?.compare_at_incl_taxes ?? price?.compare_at ?? null;
2908
3629
  }
3630
+ /** The options the shopper has to choose (those with values), in order. */
3631
+ choosableOptions() {
3632
+ return (this.product?.options ?? []).filter((o) => normalizeValues(o.values).length > 0);
3633
+ }
3634
+ /** The first option still without a value, or null when every one is chosen. */
3635
+ missingOption() {
3636
+ return this.choosableOptions().find((o) => !String(this.selectedOptions[o.name] ?? "").trim()) ?? null;
3637
+ }
3638
+ /**
3639
+ * Units that can be sold. With every option chosen, the chosen variant's;
3640
+ * before that, the most any variant still matching the partial choice has
3641
+ * — so the stepper and «Utsolgt» do not speak for a variant (the first
3642
+ * one) the shopper never picked.
3643
+ */
2909
3644
  get availableQuantity() {
3645
+ if (this.missingOption() && Array.isArray(this.product?.variants) && this.product.variants.length > 0) {
3646
+ const sel = this.selectedOptions;
3647
+ const qtys = this.product.variants.filter((v2) => this.variantMatchesSelection(v2, sel)).map((v2) => this.variantSellableQty(v2));
3648
+ return qtys.length > 0 ? Math.max(0, ...qtys) : 0;
3649
+ }
2910
3650
  const v = this.selectedVariant();
2911
3651
  if (v) return this.variantSellableQty(v);
2912
3652
  return this.product?.quantity ?? 0;
2913
3653
  }
3654
+ /**
3655
+ * «Få igjen — N stk» only for the variant actually chosen: before a size is
3656
+ * picked the count belonged to the first variant, which said «Få igjen» of
3657
+ * a product with plenty of other sizes.
3658
+ */
2914
3659
  stockLabel() {
2915
3660
  const qty = this.availableQuantity;
2916
3661
  if (qty <= 0) return { text: "Utsolgt", cls: "out-of-stock" };
2917
- if (qty <= 3) return { text: `F\xE5 igjen \u2014 ${qty} stk`, cls: "low-stock" };
3662
+ if (qty <= 3 && !this.missingOption()) return { text: `F\xE5 igjen \u2014 ${qty} stk`, cls: "low-stock" };
2918
3663
  return { text: "P\xE5 lager", cls: "in-stock" };
2919
3664
  }
3665
+ /** «Velg størrelse» for a size, «Velg farge» for a colour… */
3666
+ promptFor(option) {
3667
+ const name = String(option.name ?? "").trim();
3668
+ if (!name || /^(st(ø|o)rrelse|str\.?|size)$/i.test(name)) return "Velg st\xF8rrelse";
3669
+ return `Velg ${name.toLowerCase()}`;
3670
+ }
3671
+ /**
3672
+ * Every buy button goes through here first: with an option not chosen yet,
3673
+ * nothing is added or paid — the picker says what is missing and comes
3674
+ * into view (it sits right above the buttons, inside the buy zone).
3675
+ */
3676
+ requireSelection() {
3677
+ const missing = this.missingOption();
3678
+ if (!missing) {
3679
+ this.selectionPrompt = "";
3680
+ return true;
3681
+ }
3682
+ this.selectionPrompt = missing.name;
3683
+ void this.updateComplete.then(() => {
3684
+ const group = this.renderRoot.querySelector(".option-group.needs-choice");
3685
+ group?.scrollIntoView?.({ behavior: "smooth", block: "nearest" });
3686
+ });
3687
+ return false;
3688
+ }
2920
3689
  /** Pick an option value. If the exact combination doesn't exist, jump to an
2921
3690
  * in-stock variant that carries the value and adopt its full option map
2922
3691
  * (keeps the selection always resolvable to a real variant). */
2923
3692
  selectOption(optionName, value) {
3693
+ this.applyOption(optionName, value);
3694
+ if (this.selectionPrompt && !this.missingOption()) this.selectionPrompt = "";
3695
+ else if (this.selectionPrompt) this.selectionPrompt = this.missingOption()?.name ?? "";
3696
+ const max = this.availableQuantity;
3697
+ if (max > 0 && this.quantity > max) this.quantity = max;
3698
+ }
3699
+ applyOption(optionName, value) {
2924
3700
  const newSelection = { ...this.selectedOptions, [optionName]: value };
2925
3701
  const exactInStock = this.product?.variants?.find(
2926
3702
  (v) => this.variantMatchesSelection(v, newSelection) && this.variantSellableQty(v) > 0
@@ -2988,6 +3764,7 @@ var VioProductDetail = class extends LitElement {
2988
3764
  }
2989
3765
  async addToCart() {
2990
3766
  if (!this.product || this.availableQuantity <= 0 || this.adding) return;
3767
+ if (!this.requireSelection()) return;
2991
3768
  this.adding = true;
2992
3769
  try {
2993
3770
  const variantId = this.addCurrentToCart();
@@ -3004,11 +3781,49 @@ var VioProductDetail = class extends LitElement {
3004
3781
  }
3005
3782
  })
3006
3783
  );
3784
+ this.showAddedToast();
3007
3785
  this.close();
3008
3786
  } finally {
3009
3787
  this.adding = false;
3010
3788
  }
3011
3789
  }
3790
+ /** The toast after an add; it goes by itself after a while. */
3791
+ showAddedToast() {
3792
+ if (this.noAddedToast) return;
3793
+ this.addedToast = {
3794
+ sponsorId: this.sponsorId,
3795
+ detail: { sponsorId: this.sponsorId, express: false, ...this.attribution(), ...sponsorDetail(this.sponsorId) }
3796
+ };
3797
+ if (this.addedToastTimer) clearTimeout(this.addedToastTimer);
3798
+ this.addedToastTimer = setTimeout(() => this.hideAddedToast(), ADDED_TOAST_MS);
3799
+ }
3800
+ hideAddedToast() {
3801
+ if (this.addedToastTimer) clearTimeout(this.addedToastTimer);
3802
+ this.addedToastTimer = null;
3803
+ this.addedToast = null;
3804
+ }
3805
+ /** «Til kassen»: the checkout of the brand just added to — the host opens it on `vio:checkout-open`, as from the cart. */
3806
+ toastToCheckout() {
3807
+ const toast = this.addedToast;
3808
+ this.hideAddedToast();
3809
+ if (!toast) return;
3810
+ const cart = Vio.cart.getCart(toast.sponsorId);
3811
+ const total = (cart?.items ?? []).reduce((sum, i) => sum + i.unitPrice * i.quantity, 0);
3812
+ if (!cart || cart.items.length === 0 || !(total > 0)) return;
3813
+ this.dispatchEvent(new CustomEvent("vio:checkout-open", { bubbles: true, composed: true, detail: toast.detail }));
3814
+ }
3815
+ renderAddedToast() {
3816
+ if (!this.addedToast) return nothing;
3817
+ return html`
3818
+ <div class="added-toast" part="added-toast" role="status" aria-live="polite">
3819
+ <div class="added-toast-text">Lagt i handlekurven</div>
3820
+ <div class="added-toast-actions">
3821
+ <button class="added-toast-checkout" part="added-toast-checkout" @click=${this.toastToCheckout}>Til kassen</button>
3822
+ <button class="added-toast-continue" part="added-toast-continue" @click=${this.hideAddedToast}>Fortsett å handle</button>
3823
+ </div>
3824
+ </div>
3825
+ `;
3826
+ }
3012
3827
  /**
3013
3828
  * Express buy: add to cart, then open the checkout overlay with Klarna
3014
3829
  * preselected. The host app handles `vio:checkout-open` (same event the cart
@@ -3017,6 +3832,7 @@ var VioProductDetail = class extends LitElement {
3017
3832
  */
3018
3833
  async buyWithKlarna() {
3019
3834
  if (!this.product || this.availableQuantity <= 0 || this.adding) return;
3835
+ if (!this.requireSelection()) return;
3020
3836
  this.adding = true;
3021
3837
  try {
3022
3838
  this.addCurrentToCart();
@@ -3049,6 +3865,7 @@ var VioProductDetail = class extends LitElement {
3049
3865
  */
3050
3866
  buyNow() {
3051
3867
  if (!this.product || this.availableQuantity <= 0 || this.adding) return;
3868
+ if (!this.requireSelection()) return;
3052
3869
  this.addCurrentToCart();
3053
3870
  this.close();
3054
3871
  this.dispatchEvent(
@@ -3071,6 +3888,8 @@ var VioProductDetail = class extends LitElement {
3071
3888
  }
3072
3889
  buyWithVipps() {
3073
3890
  if (!this.product || this.availableQuantity <= 0 || this.adding) return;
3891
+ if (!this.requireSelection()) return;
3892
+ if (!(this.unitPrice > 0)) return;
3074
3893
  if ((Vio.cart.getCart(this.sponsorId)?.items.length ?? 0) > 0) {
3075
3894
  this.addCurrentToCart();
3076
3895
  this.close();
@@ -3082,6 +3901,14 @@ var VioProductDetail = class extends LitElement {
3082
3901
  this.close();
3083
3902
  void this.startVippsExpressOrCheckout(redirect);
3084
3903
  }
3904
+ /**
3905
+ * The Vipps button will open the app with Express (see buyWithVipps): the
3906
+ * brand has Express on, and this brand's cart is empty — with lines in it
3907
+ * the checkout opens first.
3908
+ */
3909
+ vippsGoesStraightToApp() {
3910
+ return this.vippsExpress && (Vio.cart.getCart(this.sponsorId)?.items.length ?? 0) === 0;
3911
+ }
3085
3912
  async startVippsExpressOrCheckout(redirect) {
3086
3913
  let fallback = true;
3087
3914
  try {
@@ -3118,6 +3945,7 @@ var VioProductDetail = class extends LitElement {
3118
3945
  /** Buy with Stripe — opens the checkout with Stripe preselected. */
3119
3946
  buyWithStripe() {
3120
3947
  if (!this.product || this.availableQuantity <= 0 || this.adding) return;
3948
+ if (!this.requireSelection()) return;
3121
3949
  this.addCurrentToCart();
3122
3950
  this.close();
3123
3951
  this.dispatchEvent(
@@ -3162,6 +3990,8 @@ var VioProductDetail = class extends LitElement {
3162
3990
  */
3163
3991
  buyWithApplePay() {
3164
3992
  if (!this.product || this.availableQuantity <= 0 || !this.applePayHandle) return;
3993
+ if (!this.requireSelection()) return;
3994
+ if (!(this.unitPrice > 0)) return;
3165
3995
  const cartHasLines = (Vio.cart.getCart(this.sponsorId)?.items.length ?? 0) > 0;
3166
3996
  if (cartHasLines || this.applePayAmount !== this.unitPrice * this.quantity) {
3167
3997
  this.addCurrentToCart();
@@ -3181,6 +4011,175 @@ var VioProductDetail = class extends LitElement {
3181
4011
  void this.applePayHandle.show();
3182
4012
  this.close();
3183
4013
  }
4014
+ /**
4015
+ * The product's cheapest rate for the page's market — the rates the
4016
+ * backend cart offers for its line. A rate in another currency than the
4017
+ * product's price is not shown (never a price the shopper would not pay).
4018
+ */
4019
+ async loadShippingFrom(productId, seq) {
4020
+ let rates = null;
4021
+ try {
4022
+ const product = Vio.commerceFor(this.sponsorId).channel.product;
4023
+ rates = typeof product.getShippingRates === "function" ? await product.getShippingRates(productId, { currency: this.currency }) : null;
4024
+ } catch {
4025
+ rates = null;
4026
+ }
4027
+ if (seq !== this.fetchSeq) return;
4028
+ const currency = String(this.product?.price?.currency_code ?? this.currency ?? "").toUpperCase();
4029
+ const usable = (rates ?? []).filter((r) => !r.currency || r.currency.toUpperCase() === currency);
4030
+ const cheapest = usable[0];
4031
+ this.shippingFrom = cheapest ? { price: cheapest.price, currency: cheapest.currency || currency } : null;
4032
+ this.shippingRates = usable.map((r) => ({
4033
+ name: String(r.name ?? ""),
4034
+ price: r.price,
4035
+ currency: r.currency || currency,
4036
+ description: String(r.description ?? "").trim(),
4037
+ logoUrl: String(r.logoUrl ?? "")
4038
+ }));
4039
+ }
4040
+ /**
4041
+ * `gallery="carousel"`: every photo in one row that snaps one at a time —
4042
+ * swipe on touch, arrows with a mouse — and a dot per photo over it
4043
+ * instead of thumbnails, so the name, price and sizes come up higher.
4044
+ */
4045
+ renderCarousel(images, title) {
4046
+ const count = images.length;
4047
+ const current2 = Math.min(this.activeImageIndex, Math.max(0, count - 1));
4048
+ return html`
4049
+ <div class="gallery carousel" part="gallery">
4050
+ <div class="carousel-track" part="image" @scroll=${this.onCarouselScroll}>
4051
+ ${images.map(
4052
+ (img, idx) => html`
4053
+ <div class="carousel-slide" part="slide" aria-label=${`Bilde ${idx + 1} av ${count}`}>
4054
+ <img src=${img.url} alt=${idx === 0 ? title : ""} loading=${idx === 0 ? "eager" : "lazy"} />
4055
+ </div>
4056
+ `
4057
+ )}
4058
+ </div>
4059
+ ${count > 1 ? html`
4060
+ <button
4061
+ class="carousel-arrow prev"
4062
+ part="carousel-arrow"
4063
+ aria-label="Forrige bilde"
4064
+ ?disabled=${current2 === 0}
4065
+ @click=${() => this.stepCarousel(-1)}
4066
+ >‹</button>
4067
+ <button
4068
+ class="carousel-arrow next"
4069
+ part="carousel-arrow"
4070
+ aria-label="Neste bilde"
4071
+ ?disabled=${current2 === count - 1}
4072
+ @click=${() => this.stepCarousel(1)}
4073
+ >›</button>
4074
+ <div class="carousel-dots" part="image-dots" aria-hidden="true">
4075
+ ${images.map((_, idx) => html`<span class="carousel-dot ${idx === current2 ? "active" : ""}"></span>`)}
4076
+ </div>
4077
+ ` : ""}
4078
+ </div>
4079
+ `;
4080
+ }
4081
+ /**
4082
+ * «Beskrivelse», not shut: the first lines show, fading out, and a tap on
4083
+ * the heading, the text or «Les mer» opens the rest (Angelo, 2026-10-28).
4084
+ * A description that fits in the preview has nothing to open.
4085
+ */
4086
+ renderDescription(text) {
4087
+ const mode = this.resolvedDescriptionMode;
4088
+ if (mode === "open") {
4089
+ return html`<div class="description-block description-preview open" part="description">
4090
+ <div class="desc-head">Beskrivelse</div>
4091
+ <div class="description">${text}</div>
4092
+ </div>`;
4093
+ }
4094
+ const foldable = mode === "folded" || this.descriptionClipped;
4095
+ const open = this.descriptionOpen || !foldable;
4096
+ const toggle = () => {
4097
+ if (foldable) this.descriptionOpen = !this.descriptionOpen;
4098
+ };
4099
+ return html`<div class="description-block description-preview ${open ? "open" : ""} ${foldable ? "clipped" : ""} ${mode}" part="description">
4100
+ <button class="desc-head" type="button" aria-expanded=${open ? "true" : "false"} @click=${toggle}>Beskrivelse</button>
4101
+ ${mode === "folded" && !open ? nothing : html`<div class="description ${open ? "" : "collapsed"}" @click=${() => !this.descriptionOpen && toggle()}>${text}</div>`}
4102
+ ${mode === "preview" && this.descriptionClipped ? html`<button class="desc-more" type="button" @click=${toggle}>${this.descriptionOpen ? "Vis mindre" : "Les mer"}</button>` : nothing}
4103
+ </div>`;
4104
+ }
4105
+ /** Is the preview hiding part of the text? Measured with the preview applied. */
4106
+ measureDescription() {
4107
+ const el = this.renderRoot.querySelector(".description-preview .description");
4108
+ if (!el || this.descriptionOpen) return;
4109
+ const wasCollapsed = el.classList.contains("collapsed");
4110
+ el.classList.add("collapsed");
4111
+ const clipped = el.scrollHeight > el.clientHeight + 1;
4112
+ if (!wasCollapsed) el.classList.remove("collapsed");
4113
+ if (clipped !== this.descriptionClipped) this.descriptionClipped = clipped;
4114
+ }
4115
+ renderHighlights() {
4116
+ const own = (Array.isArray(this.highlights) ? this.highlights : []).filter((h) => h && h.title);
4117
+ const items = own.length ? own : resolveSponsorIdentity(this.sponsorId)?.highlights ?? [];
4118
+ if (items.length === 0) return nothing;
4119
+ return html`<ul class="highlights" part="highlights">
4120
+ ${items.map((h) => {
4121
+ const path = h.icon ? HIGHLIGHT_ICONS[h.icon] : void 0;
4122
+ const icon = path ? html`<svg class="hl-icon" viewBox="0 0 24 24" aria-hidden="true"><path d=${path}></path></svg>` : h.icon ? html`<img class="hl-icon" src=${h.icon} alt="" />` : nothing;
4123
+ return html`<li class="hl" part="highlight">
4124
+ ${icon}
4125
+ <span class="hl-title">${h.title}</span>
4126
+ ${h.text ? html`<span class="hl-text">${h.text}</span>` : nothing}
4127
+ </li>`;
4128
+ })}
4129
+ </ul>`;
4130
+ }
4131
+ /**
4132
+ * «Frakt & retur», folded like «Beskrivelse»: the delivery options for the
4133
+ * page's country with their price, and the right of withdrawal with the
4134
+ * brand's own terms and returns pages when it gave them.
4135
+ */
4136
+ renderShippingReturns() {
4137
+ const identity = resolveSponsorIdentity(this.sponsorId);
4138
+ const rates = this.shippingRates;
4139
+ const times = [...new Set(rates.map((r) => r.description).filter(Boolean))];
4140
+ const perRateTime = times.length > 1;
4141
+ const sharedTime = times.length === 1 ? times[0] : times.length === 0 && this.deliveryTime.trim() ? `Leveringstid: ${this.deliveryTime.trim()}` : "";
4142
+ const links = [
4143
+ identity?.refundPolicyUrl ? html`<a href=${identity.refundPolicyUrl} target="_blank" rel="noopener noreferrer">Angrerett og retur</a>` : null,
4144
+ identity?.termsUrl ? html`<a href=${identity.termsUrl} target="_blank" rel="noopener noreferrer">Kjøpsvilkår</a>` : null
4145
+ ].filter(Boolean);
4146
+ return html`<details class="description-block shipping-returns" part="shipping-returns">
4147
+ <summary>Frakt &amp; retur</summary>
4148
+ <div class="description shipping-returns-body">
4149
+ <div class="sr-title">Frakt</div>
4150
+ ${rates.length ? html`<ul class="sr-rates">
4151
+ ${rates.map((r) => {
4152
+ const { carrier, method } = splitRateName(r.name);
4153
+ const logo = r.logoUrl || carrierLogo(this.carrierLogos, carrier);
4154
+ return html`<li>
4155
+ ${logo ? html`<img class="sr-logo" src=${logo} alt=${carrier} />` : lineIcon(rateIcon(r.name ?? ""), "line-icon sr-icon")}
4156
+ <span class="sr-rate">
4157
+ <span class="sr-carrier">${carrier || method || "Frakt"}</span>
4158
+ ${carrier && method ? html`<span class="sr-method">${method}</span>` : nothing}
4159
+ ${perRateTime && r.description ? html`<span class="sr-method">${r.description}</span>` : nothing}
4160
+ </span>
4161
+ <span class="sr-price">${r.price <= 0 ? "Gratis" : formatPrice(r.price, r.currency)}</span>
4162
+ </li>`;
4163
+ })}
4164
+ </ul>` : html`<p>Fraktprisen vises i kassen.</p>`}
4165
+ ${sharedTime ? html`<p class="sr-line sr-time">${lineIcon("clock", "line-icon sr-icon")}<span>${sharedTime}</span></p>` : nothing}
4166
+ <div class="sr-title">Retur</div>
4167
+ <p class="sr-line">${lineIcon("return", "line-icon sr-icon")}<span>Du har 14 dagers angrerett fra du mottar varen.</span></p>
4168
+ ${links.length ? html`<p class="sr-links">${links}</p>` : nothing}
4169
+ </div>
4170
+ </details>`;
4171
+ }
4172
+ /** Under the price: «Frakt fra X · 14 dagers angrerett» (or «Gratis frakt · …»). */
4173
+ renderDeliveryNote() {
4174
+ const ship = this.shippingFrom;
4175
+ const shipping = ship ? ship.price <= 0 ? "Gratis frakt" : `Frakt fra ${formatPrice(ship.price, ship.currency)}` : "";
4176
+ return html`<div class="delivery-note" part="delivery-note">
4177
+ ${shipping ? html`<span part="delivery-shipping">${shipping}</span><span aria-hidden="true"> · </span>` : ""}<span
4178
+ part="delivery-withdrawal"
4179
+ >14 dagers angrerett</span
4180
+ >
4181
+ </div>`;
4182
+ }
3184
4183
  /** The seller's Company name; the brand when there is none; nothing while asking. */
3185
4184
  headerName() {
3186
4185
  if (this.sellerName) return this.sellerName;
@@ -3263,6 +4262,7 @@ var VioProductDetail = class extends LitElement {
3263
4262
 
3264
4263
  ${this.renderBody()}
3265
4264
  </div>
4265
+ ${this.renderAddedToast()}
3266
4266
  `;
3267
4267
  }
3268
4268
  renderBody() {
@@ -3290,7 +4290,7 @@ var VioProductDetail = class extends LitElement {
3290
4290
  const activeImage = this.activeImage;
3291
4291
  return html`
3292
4292
  <div class="body">
3293
- <div class="gallery" part="gallery">
4293
+ ${this.galleryMode === "carousel" ? this.renderCarousel(images, p.title) : html`<div class="gallery" part="gallery">
3294
4294
  <div class="gallery-main" part="image">
3295
4295
  ${activeImage?.url ? html`<img src=${activeImage.url} alt=${p.title} />` : ""}
3296
4296
  </div>
@@ -3327,26 +4327,30 @@ var VioProductDetail = class extends LitElement {
3327
4327
  >›</button>
3328
4328
  </div>
3329
4329
  ` : ""}
3330
- </div>
4330
+ </div>`}
3331
4331
 
3332
4332
  <div class="info" part="info">
3333
4333
  ${p.brand ? html`<div class="brand" part="brand">${p.brand}</div>` : ""}
3334
- <h2 class="name" part="title">${p.title}</h2>
3335
-
3336
- <div class="price-row">
3337
- <span class="price" part="price">${formatPrice(this.unitPrice, p.price.currency_code)}</span>
3338
- ${showCompare ? html`<span class="compare-at">${formatPrice(
4334
+ <div class="headline">
4335
+ <div class="title-row">
4336
+ <h2 class="name" part="title">${p.title}</h2>
4337
+ <div class="stock ${stock.cls}" part="stock">${stock.text}</div>
4338
+ </div>
4339
+ <div class="price-row">
4340
+ <span class="price" part="price">${formatPrice(this.unitPrice, p.price.currency_code)}</span>
4341
+ ${showCompare ? html`<span class="compare-at">${formatPrice(
3339
4342
  compare,
3340
4343
  p.price.currency_code
3341
4344
  )}</span>` : ""}
4345
+ </div>
3342
4346
  </div>
3343
4347
 
3344
- <div class="stock ${stock.cls}" part="stock">${stock.text}</div>
3345
-
3346
- ${this.renderOptionPickers(p.options)}
3347
-
4348
+ <div class="purchase">
3348
4349
  <div class="buy" part="buy">
3349
- ${this.availableQuantity > 0 && this.methodEnabled("vipps") ? this.vippsOfficial ? html`
4350
+ ${p.options.length > 0 ? html`<div class="buy-options" part="buy-options">${this.renderOptionPickers(p.options)}</div>` : ""}
4351
+ ${this.renderDeliveryNote()}
4352
+ ${this.methodsPending && this.availableQuantity > 0 ? html`<div class="buy-pending" part="buy-pending" role="status" aria-label="Laster betalingsmåter"></div>` : ""}
4353
+ ${this.availableQuantity > 0 && this.hasPrice && this.methodEnabled("vipps") ? this.vippsOfficial ? html`
3350
4354
  <vipps-mobilepay-button
3351
4355
  class="buy-vipps-official"
3352
4356
  part="buy-vipps"
@@ -3372,6 +4376,7 @@ var VioProductDetail = class extends LitElement {
3372
4376
  <span class="vipps-badge">vipps</span>
3373
4377
  </button>
3374
4378
  ` : ""}
4379
+ ${this.availableQuantity > 0 && this.hasPrice && this.methodEnabled("vipps") && this.vippsGoesStraightToApp() ? html`<p class="vipps-note" part="vipps-note">Vipps fyller inn navn og adresse – du godkjenner og betaler i appen</p>` : ""}
3375
4380
 
3376
4381
  <div class="qty-cta-row" part="quantity-row">
3377
4382
  <div class="qty-stepper">
@@ -3413,7 +4418,7 @@ var VioProductDetail = class extends LitElement {
3413
4418
  </button>
3414
4419
  ` : ""}
3415
4420
 
3416
- ${this.availableQuantity > 0 && this.applePayOk && this.methodEnabled("apple-pay", "applepay") ? html`
4421
+ ${this.availableQuantity > 0 && this.hasPrice && this.applePayOk && this.methodEnabled("apple-pay", "applepay") ? html`
3417
4422
  <button
3418
4423
  class="buy-applepay"
3419
4424
  @click=${this.buyWithApplePay}
@@ -3459,12 +4464,11 @@ var VioProductDetail = class extends LitElement {
3459
4464
  ${this.renderSellerNote()}
3460
4465
  </div>
3461
4466
 
3462
- ${p.description ? html`
3463
- <details class="description-block" part="description">
3464
- <summary>Beskrivelse</summary>
3465
- <div class="description">${this.stripHtml(p.description)}</div>
3466
- </details>
3467
- ` : ""}
4467
+ ${this.renderHighlights()}
4468
+
4469
+ ${p.description ? this.renderDescription(this.stripHtml(p.description)) : ""}
4470
+ ${this.renderShippingReturns()}
4471
+ </div>
3468
4472
  </div>
3469
4473
  </div>
3470
4474
  `;
@@ -3474,9 +4478,16 @@ var VioProductDetail = class extends LitElement {
3474
4478
  return html`${options.map((opt) => {
3475
4479
  const values = normalizeValues(opt.values);
3476
4480
  if (values.length === 0) return "";
4481
+ const chosen = String(this.selectedOptions[opt.name] ?? "").trim();
4482
+ const asked = this.selectionPrompt === opt.name && !chosen;
3477
4483
  return html`
3478
- <div class="option-group" part="option-group">
3479
- <div class="option-label" part="option-label">${opt.name}</div>
4484
+ <div class="option-group ${asked ? "needs-choice" : ""}" part="option-group">
4485
+ <div class="option-label" part="option-label">
4486
+ <span
4487
+ >${opt.name}${chosen ? html`: <span class="option-chosen" part="option-chosen">${chosen}</span>` : ""}</span
4488
+ >
4489
+ ${asked ? html`<span class="option-prompt" part="option-prompt" role="alert">${this.promptFor(opt)}</span>` : ""}
4490
+ </div>
3480
4491
  <div class="option-values" part="options">
3481
4492
  ${values.map((val) => {
3482
4493
  const available = this.isValueAvailable(opt.name, val);
@@ -3541,6 +4552,24 @@ __decorateClass([
3541
4552
  __decorateClass([
3542
4553
  property({ type: String, attribute: "vio-content-title" })
3543
4554
  ], VioProductDetail.prototype, "vioContentTitle", 2);
4555
+ __decorateClass([
4556
+ property({ type: Boolean, attribute: "no-added-toast" })
4557
+ ], VioProductDetail.prototype, "noAddedToast", 2);
4558
+ __decorateClass([
4559
+ property({ type: String })
4560
+ ], VioProductDetail.prototype, "gallery", 2);
4561
+ __decorateClass([
4562
+ property({ type: String, attribute: "description-mode" })
4563
+ ], VioProductDetail.prototype, "descriptionMode", 2);
4564
+ __decorateClass([
4565
+ property({ type: Array })
4566
+ ], VioProductDetail.prototype, "highlights", 2);
4567
+ __decorateClass([
4568
+ property({ type: String, attribute: "delivery-time" })
4569
+ ], VioProductDetail.prototype, "deliveryTime", 2);
4570
+ __decorateClass([
4571
+ property({ type: Object, attribute: "carrier-logos" })
4572
+ ], VioProductDetail.prototype, "carrierLogos", 2);
3544
4573
  __decorateClass([
3545
4574
  state()
3546
4575
  ], VioProductDetail.prototype, "product", 2);
@@ -3550,6 +4579,12 @@ __decorateClass([
3550
4579
  __decorateClass([
3551
4580
  state()
3552
4581
  ], VioProductDetail.prototype, "sellerSettled", 2);
4582
+ __decorateClass([
4583
+ state()
4584
+ ], VioProductDetail.prototype, "shippingFrom", 2);
4585
+ __decorateClass([
4586
+ state()
4587
+ ], VioProductDetail.prototype, "shippingRates", 2);
3553
4588
  __decorateClass([
3554
4589
  state()
3555
4590
  ], VioProductDetail.prototype, "isLoading", 2);
@@ -3559,6 +4594,9 @@ __decorateClass([
3559
4594
  __decorateClass([
3560
4595
  state()
3561
4596
  ], VioProductDetail.prototype, "selectedOptions", 2);
4597
+ __decorateClass([
4598
+ state()
4599
+ ], VioProductDetail.prototype, "selectionPrompt", 2);
3562
4600
  __decorateClass([
3563
4601
  state()
3564
4602
  ], VioProductDetail.prototype, "quantity", 2);
@@ -3574,18 +4612,39 @@ __decorateClass([
3574
4612
  __decorateClass([
3575
4613
  state()
3576
4614
  ], VioProductDetail.prototype, "thumbsAtEnd", 2);
4615
+ __decorateClass([
4616
+ state()
4617
+ ], VioProductDetail.prototype, "descriptionOpen", 2);
4618
+ __decorateClass([
4619
+ state()
4620
+ ], VioProductDetail.prototype, "descriptionClipped", 2);
4621
+ __decorateClass([
4622
+ state()
4623
+ ], VioProductDetail.prototype, "galleryMode", 2);
4624
+ __decorateClass([
4625
+ state()
4626
+ ], VioProductDetail.prototype, "resolvedDescriptionMode", 2);
3577
4627
  __decorateClass([
3578
4628
  state()
3579
4629
  ], VioProductDetail.prototype, "logoSquare", 2);
3580
4630
  __decorateClass([
3581
4631
  state()
3582
4632
  ], VioProductDetail.prototype, "adding", 2);
4633
+ __decorateClass([
4634
+ state()
4635
+ ], VioProductDetail.prototype, "addedToast", 2);
3583
4636
  __decorateClass([
3584
4637
  state()
3585
4638
  ], VioProductDetail.prototype, "applePayOk", 2);
3586
4639
  __decorateClass([
3587
4640
  state()
3588
4641
  ], VioProductDetail.prototype, "availableMethods", 2);
4642
+ __decorateClass([
4643
+ state()
4644
+ ], VioProductDetail.prototype, "methodsLoading", 2);
4645
+ __decorateClass([
4646
+ state()
4647
+ ], VioProductDetail.prototype, "vippsExpress", 2);
3589
4648
  __decorateClass([
3590
4649
  state()
3591
4650
  ], VioProductDetail.prototype, "vippsOfficial", 2);
@@ -3598,9 +4657,35 @@ function normalizeValues(raw) {
3598
4657
  }
3599
4658
  return [];
3600
4659
  }
4660
+ function splitRateName(name) {
4661
+ const text = String(name ?? "").trim();
4662
+ const m = text.match(/^(.+?)\s+[-–—]\s+(.+)$/);
4663
+ if (!m?.[1] || !m[2]) return { carrier: "", method: text };
4664
+ const method = m[2].trim();
4665
+ return { carrier: m[1].trim(), method: method.charAt(0).toUpperCase() + method.slice(1) };
4666
+ }
3601
4667
  var STRIPE_LOGO_SRC2 = "data:image/svg+xml;utf8," + encodeURIComponent(
3602
4668
  '<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>'
3603
4669
  );
4670
+ function ratePrice(rate, currency) {
4671
+ const price = rate?.price;
4672
+ if (!price) return null;
4673
+ const amount = typeof price.amount_incl_taxes === "number" ? price.amount_incl_taxes : price.amount;
4674
+ if (typeof amount !== "number" || !Number.isFinite(amount) || amount < 0) return null;
4675
+ if (price.currency_code && currency && price.currency_code.toUpperCase() !== currency.toUpperCase()) return null;
4676
+ return amount;
4677
+ }
4678
+ function cartShipping(items, currency) {
4679
+ for (const item of items) {
4680
+ const chosen = ratePrice(item.shipping, currency);
4681
+ if (chosen !== null) return chosen;
4682
+ }
4683
+ for (const item of items) {
4684
+ const offered = (item.availableShippings ?? []).map((r) => ratePrice(r, currency)).filter((p) => p !== null);
4685
+ if (offered.length > 0) return Math.min(...offered);
4686
+ }
4687
+ return null;
4688
+ }
3604
4689
  var VioCart = class extends LitElement {
3605
4690
  constructor() {
3606
4691
  super(...arguments);
@@ -3612,6 +4697,7 @@ var VioCart = class extends LitElement {
3612
4697
  this.itemCount = 0;
3613
4698
  this.applePayAvailable = false;
3614
4699
  this.availableMethods = null;
4700
+ this.methodsLoading = false;
3615
4701
  this.vippsOfficial = false;
3616
4702
  /** Whether the method lookup has been attempted — see boundOnCartChange. */
3617
4703
  this.paymentMethodsAttempted = false;
@@ -3634,6 +4720,7 @@ var VioCart = class extends LitElement {
3634
4720
  sheet: () => this.renderRoot.querySelector(".drawer"),
3635
4721
  backdrop: () => this.renderRoot.querySelector(".backdrop"),
3636
4722
  dragZones: ".handle, .header",
4723
+ scroller: () => this.renderRoot.querySelector(".body"),
3637
4724
  close: () => this.close()
3638
4725
  });
3639
4726
  this.boundOnCartChange = (e) => {
@@ -3838,6 +4925,14 @@ var VioCart = class extends LitElement {
3838
4925
  cursor: pointer;
3839
4926
  }
3840
4927
  .checkout-btn:hover { opacity: 0.9; }
4928
+ .checkout-btn:disabled { opacity: 0.5; cursor: not-allowed; }
4929
+ /* Where the express buttons go while the brand's methods load. */
4930
+ .methods-pending {
4931
+ height: 48px;
4932
+ margin-bottom: 10px;
4933
+ border-radius: var(--vio-radius-lg, 8px);
4934
+ background: var(--vio-color-surface-muted, #f1f1f1);
4935
+ }
3841
4936
  .footer {
3842
4937
  padding: var(--vio-space-lg, 24px);
3843
4938
  border-top: 1px solid var(--vio-color-border, #e5e5e5);
@@ -3850,6 +4945,14 @@ var VioCart = class extends LitElement {
3850
4945
  font-size: 14px;
3851
4946
  }
3852
4947
  .subtotal-label { color: var(--vio-color-text-secondary, #666); }
4948
+ .shipping-row,
4949
+ .total-row {
4950
+ display: flex;
4951
+ justify-content: space-between;
4952
+ margin: calc(var(--vio-space-md, 16px) * -0.5) 0 var(--vio-space-md, 16px);
4953
+ font-size: 14px;
4954
+ }
4955
+ .total-row { font-weight: 700; font-size: 16px; margin-top: 0; }
3853
4956
  .subtotal-value { font-weight: 700; font-size: 16px; }
3854
4957
 
3855
4958
  /* Apple Pay express — pay the whole cart directly. */
@@ -4045,6 +5148,7 @@ var VioCart = class extends LitElement {
4045
5148
  this.loadingPaymentMethods = false;
4046
5149
  return;
4047
5150
  }
5151
+ this.methodsLoading = true;
4048
5152
  try {
4049
5153
  const methods = await Vio.checkout.getAvailablePaymentMethods(firstSponsorId);
4050
5154
  if (Array.isArray(methods)) {
@@ -4059,10 +5163,20 @@ var VioCart = class extends LitElement {
4059
5163
  this.availableMethods = null;
4060
5164
  } finally {
4061
5165
  this.loadingPaymentMethods = false;
5166
+ this.methodsLoading = false;
4062
5167
  }
4063
5168
  }
4064
- /** Backend names arrive as e.g. "Apple Pay" — compare letters only. */
5169
+ /** The methods are on their way and none is known yet. */
5170
+ get methodsPending() {
5171
+ return this.methodsLoading && this.availableMethods === null;
5172
+ }
5173
+ /**
5174
+ * Backend names arrive as e.g. "Apple Pay" — compare letters only. While
5175
+ * the list loads, no method's button is drawn: «unknown = all» painted
5176
+ * Stripe and Klarna for a Vipps-only brand until it came.
5177
+ */
4065
5178
  methodEnabled(...names) {
5179
+ if (this.methodsPending) return false;
4066
5180
  return isMethodEnabled(this.availableMethods, ...names);
4067
5181
  }
4068
5182
  updated(changed) {
@@ -4105,7 +5219,7 @@ var VioCart = class extends LitElement {
4105
5219
  /** Apple Pay express — pay the whole cart, then show the confirmation. */
4106
5220
  onApplePay() {
4107
5221
  const handle = this.applePayHandle;
4108
- if (!handle || !handle.available || this.itemCount === 0) return;
5222
+ if (!handle || !handle.available || !this.payable) return;
4109
5223
  const snapshot = this.buildConfirmation();
4110
5224
  const sponsorIds = [...this.carts.keys()];
4111
5225
  const sponsor = sponsorIds.length === 1 ? sponsorDetail(sponsorIds[0]) : {};
@@ -4149,7 +5263,7 @@ var VioCart = class extends LitElement {
4149
5263
  * (betalingsmetode). The host's checkout-open handler closes this cart and
4150
5264
  * opens <vio-checkout> in express mode with Klarna preselected. */
4151
5265
  onKlarnaExpress() {
4152
- if (this.itemCount === 0) return;
5266
+ if (!this.payable) return;
4153
5267
  const firstSponsorId = [...this.carts.keys()][0];
4154
5268
  if (firstSponsorId === void 0) return;
4155
5269
  this.dispatchEvent(
@@ -4170,7 +5284,7 @@ var VioCart = class extends LitElement {
4170
5284
  * method present the others were unreachable from here.
4171
5285
  */
4172
5286
  onCheckout() {
4173
- if (this.itemCount === 0) return;
5287
+ if (!this.payable) return;
4174
5288
  const firstSponsorId = [...this.carts.keys()][0];
4175
5289
  if (firstSponsorId === void 0) return;
4176
5290
  this.dispatchEvent(
@@ -4182,51 +5296,31 @@ var VioCart = class extends LitElement {
4182
5296
  );
4183
5297
  }
4184
5298
  /**
4185
- * Vipps — Express: the app collects address and delivery, so the cart
4186
- * goes straight to Vipps. When this cart cannot have Express (several
4187
- * suppliers, digital goods, no rate) or Vipps cannot be started, the
4188
- * checkout opens with Vipps preselected, as before.
5299
+ * Vipps from the cart: the checkout opens with Vipps preselected — the
5300
+ * standard path, where our form collects name, address, mobile and the
5301
+ * delivery (with its price) before paying (Mote & Livsstil, 2026-10-28).
5302
+ * It no longer starts Express from here: Express is the product detail's
5303
+ * «Kjøp nå med Vipps» only.
4189
5304
  */
4190
5305
  onVipps() {
4191
- if (this.itemCount === 0) return;
5306
+ if (!this.payable) return;
4192
5307
  const firstSponsorId = [...this.carts.keys()][0];
4193
5308
  if (firstSponsorId === void 0) return;
4194
- const redirect = beginPaymentRedirect({ method: "vipps", sponsorId: firstSponsorId, source: "cart" });
4195
- void this.startVippsExpressOrCheckout(firstSponsorId, redirect);
4196
- }
4197
- /** Vipps' own button when its script answers; our orange one otherwise. */
4198
- async loadVippsButton() {
4199
- this.vippsOfficial = await ensureVippsButton();
4200
- }
4201
- async startVippsExpressOrCheckout(sponsorId, redirect) {
4202
- let fallback = true;
4203
- try {
4204
- await Vio.cart.waitForCartMutations(sponsorId);
4205
- if (!await Vio.checkout.getVippsExpressEnabled(sponsorId)) {
4206
- redirect.abort("express-off");
4207
- } else {
4208
- const res = await Vio.checkout.startVippsExpress(sponsorId);
4209
- fallback = !res?.payment_url;
4210
- if (fallback) redirect.abort("not-started");
4211
- }
4212
- } catch (err) {
4213
- if (typeof console !== "undefined") {
4214
- console.warn("[VioCart] Vipps Express not started:", err);
4215
- }
4216
- redirect.fail(err instanceof Error ? err.message : String(err));
4217
- }
4218
- if (!fallback) return;
4219
5309
  this.dispatchEvent(
4220
5310
  new CustomEvent("vio:checkout-open", {
4221
5311
  bubbles: true,
4222
5312
  composed: true,
4223
- detail: { sponsorId, paymentMethod: "vipps", express: false, ...sponsorDetail(sponsorId) }
5313
+ detail: { sponsorId: firstSponsorId, paymentMethod: "vipps", express: false, ...sponsorDetail(firstSponsorId) }
4224
5314
  })
4225
5315
  );
4226
5316
  }
5317
+ /** Vipps' own button when its script answers; our orange one otherwise. */
5318
+ async loadVippsButton() {
5319
+ this.vippsOfficial = await ensureVippsButton();
5320
+ }
4227
5321
  /** Stripe — opens the checkout with Stripe preselected. */
4228
5322
  onStripe() {
4229
- if (this.itemCount === 0) return;
5323
+ if (!this.payable) return;
4230
5324
  const firstSponsorId = [...this.carts.keys()][0];
4231
5325
  if (firstSponsorId === void 0) return;
4232
5326
  this.dispatchEvent(
@@ -4241,6 +5335,10 @@ var VioCart = class extends LitElement {
4241
5335
  sponsorLabel(cart) {
4242
5336
  return resolveSponsorIdentity(cart.sponsorId)?.name || cart.sponsorName || "";
4243
5337
  }
5338
+ /** Something to pay for: lines, and a total above 0. No button opens a checkout or a payment otherwise. */
5339
+ get payable() {
5340
+ return this.itemCount > 0 && this.totalAcrossSponsors > 0;
5341
+ }
4244
5342
  get totalAcrossSponsors() {
4245
5343
  let sum = 0;
4246
5344
  for (const cart of this.carts.values()) {
@@ -4248,6 +5346,24 @@ var VioCart = class extends LitElement {
4248
5346
  }
4249
5347
  return sum;
4250
5348
  }
5349
+ /**
5350
+ * Shipping across the carts, from what the server cart says of its lines:
5351
+ * the rate chosen for them, else the cheapest one offered (the one the
5352
+ * checkout preselects). null when some cart has no known rate — the footer
5353
+ * says it is worked out in the checkout then.
5354
+ */
5355
+ get shippingAcrossSponsors() {
5356
+ let sum = 0;
5357
+ let any = false;
5358
+ for (const cart of this.carts.values()) {
5359
+ if (cart.items.length === 0) continue;
5360
+ const rate = cartShipping(cart.items, cart.currency);
5361
+ if (rate === null) return null;
5362
+ sum += rate;
5363
+ any = true;
5364
+ }
5365
+ return any ? sum : null;
5366
+ }
4251
5367
  get primaryCurrency() {
4252
5368
  for (const cart of this.carts.values()) return cart.currency;
4253
5369
  return "NOK";
@@ -4339,13 +5455,16 @@ var VioCart = class extends LitElement {
4339
5455
  ${formatPrice(this.totalAcrossSponsors, this.primaryCurrency)}
4340
5456
  </span>
4341
5457
  </div>
5458
+ ${this.renderShippingRows()}
4342
5459
  <button
4343
5460
  class="checkout-btn"
4344
5461
  @click=${this.onCheckout}
5462
+ ?disabled=${!this.payable}
4345
5463
  aria-label="Til kassen"
4346
5464
  >
4347
5465
  Til kassen
4348
5466
  </button>
5467
+ ${this.methodsPending ? html`<div class="methods-pending" role="status" aria-label="Laster betalingsmåter"></div>` : ""}
4349
5468
  ${this.methodEnabled("apple-pay") && this.applePayAvailable ? html`
4350
5469
  <button
4351
5470
  class="applepay-btn"
@@ -4406,6 +5525,20 @@ var VioCart = class extends LitElement {
4406
5525
  </aside>
4407
5526
  `;
4408
5527
  }
5528
+ /** «Frakt» (the chosen or cheapest rate) and «Totalt» with it; «Beregnes i kassen» when unknown. */
5529
+ renderShippingRows() {
5530
+ const shipping = this.shippingAcrossSponsors;
5531
+ return html`
5532
+ <div class="shipping-row" part="cart-shipping">
5533
+ <span class="subtotal-label">Frakt</span>
5534
+ <span>${shipping === null ? "Beregnes i kassen" : shipping <= 0 ? "Gratis" : formatPrice(shipping, this.primaryCurrency)}</span>
5535
+ </div>
5536
+ ${shipping !== null ? html`<div class="total-row" part="cart-total">
5537
+ <span>Totalt</span>
5538
+ <span>${formatPrice(this.totalAcrossSponsors + shipping, this.primaryCurrency)}</span>
5539
+ </div>` : ""}
5540
+ `;
5541
+ }
4409
5542
  renderConfirmation() {
4410
5543
  const o = this.confirmedOrder;
4411
5544
  if (!o) return html``;
@@ -4457,6 +5590,9 @@ __decorateClass([
4457
5590
  __decorateClass([
4458
5591
  state()
4459
5592
  ], VioCart.prototype, "availableMethods", 2);
5593
+ __decorateClass([
5594
+ state()
5595
+ ], VioCart.prototype, "methodsLoading", 2);
4460
5596
  __decorateClass([
4461
5597
  state()
4462
5598
  ], VioCart.prototype, "vippsOfficial", 2);
@@ -4498,13 +5634,14 @@ function errorText(err) {
4498
5634
  var STRIPE_LOGO_SRC3 = "data:image/svg+xml;utf8," + encodeURIComponent(
4499
5635
  '<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>'
4500
5636
  );
5637
+ var EMPTY_CART_MESSAGE2 = "Handlekurven er tom.";
5638
+ var SHIPPING_NOT_SAVED_MESSAGE = "Kunne ikke lagre fraktmetoden. Pr\xF8v igjen.";
4501
5639
  var _VioCheckout = class _VioCheckout extends LitElement {
4502
5640
  constructor() {
4503
5641
  super(...arguments);
4504
5642
  this.open = false;
4505
5643
  this.heading = "Kasse";
4506
5644
  this.shippingLabel = "Beregnes ved kasse";
4507
- this.vippsShipsInApp = false;
4508
5645
  this.express = false;
4509
5646
  this.checkoutState = null;
4510
5647
  this.items = [];
@@ -4604,6 +5741,7 @@ var _VioCheckout = class _VioCheckout extends LitElement {
4604
5741
  firstName: "",
4605
5742
  lastName: "",
4606
5743
  email: "",
5744
+ phone: "",
4607
5745
  address: "",
4608
5746
  postalCode: "",
4609
5747
  city: ""
@@ -4964,6 +6102,8 @@ var _VioCheckout = class _VioCheckout extends LitElement {
4964
6102
  transition: border-color 0.15s;
4965
6103
  }
4966
6104
  .field input:focus { border-bottom-color: var(--vio-color-text, #0a0a0a); }
6105
+ .field input[aria-invalid='true'] { border-bottom-color: var(--vio-color-accent, #c14a3b); }
6106
+ .field-error { margin-top: 4px; font-size: 12px; color: var(--vio-color-accent, #c14a3b); }
4967
6107
 
4968
6108
  .payment-grid {
4969
6109
  display: grid;
@@ -5652,7 +6792,7 @@ var _VioCheckout = class _VioCheckout extends LitElement {
5652
6792
  this.cleanReturnQueryParams(["checkout_id", "payment_processor"]);
5653
6793
  return;
5654
6794
  }
5655
- if (vioMethod === "klarna" && typeof sessionStorage !== "undefined" && sessionStorage.getItem("vio.klarna.pending.v1")) {
6795
+ if (vioMethod === "klarna" && storageGet("session", "vio.klarna.pending.v1")) {
5656
6796
  this.cleanReturnQueryParams(["vio_payment", "vio_method", "vio_sponsor", "checkout_id"]);
5657
6797
  return;
5658
6798
  }
@@ -5919,12 +7059,34 @@ var _VioCheckout = class _VioCheckout extends LitElement {
5919
7059
  (i) => Array.isArray(i.availableShippings) && i.availableShippings.length > 0
5920
7060
  );
5921
7061
  }
5922
- /** Whether all mandatory Leveringsadresse fields are filled. */
7062
+ /** The shipping country the mobile's calling code defaults to (no +prefix typed). */
7063
+ get phoneCountry() {
7064
+ return String(this.form.country || getGlobalCountryCode());
7065
+ }
7066
+ /**
7067
+ * The mobile is required only for the standard Vipps payment — shopcart
7068
+ * does not ask Vipps for the profile there. Every other method takes it
7069
+ * when given and never waits for it (a host that pre-fills the form with
7070
+ * `setAddress()` and no phone keeps working).
7071
+ */
7072
+ get phoneRequired() {
7073
+ return this.checkoutState?.paymentMethod === "vipps";
7074
+ }
7075
+ /** A number typed that is not usable (nothing said while the field is empty). */
7076
+ get phoneError() {
7077
+ const raw = String(this.form.phone ?? "").trim();
7078
+ return raw.length > 0 && !isValidPhone(raw, this.phoneCountry);
7079
+ }
7080
+ /** The mobile as the chosen method needs it: valid when required, else anything. */
7081
+ get phoneReady() {
7082
+ return !this.phoneRequired || isValidPhone(this.form.phone, this.phoneCountry);
7083
+ }
7084
+ /** Whether all mandatory Leveringsadresse fields are filled (the mobile only for Vipps). */
5923
7085
  get isAddressValid() {
5924
7086
  const f = this.form;
5925
7087
  if (!f) return false;
5926
7088
  return Boolean(
5927
- f.firstName && String(f.firstName).trim().length > 0 && f.lastName && String(f.lastName).trim().length > 0 && f.email && String(f.email).trim().length > 0 && String(f.email).includes("@") && f.address && String(f.address).trim().length > 0 && f.postalCode && String(f.postalCode).trim().length > 0 && f.city && String(f.city).trim().length > 0
7089
+ f.firstName && String(f.firstName).trim().length > 0 && f.lastName && String(f.lastName).trim().length > 0 && f.email && String(f.email).trim().length > 0 && String(f.email).includes("@") && f.address && String(f.address).trim().length > 0 && f.postalCode && String(f.postalCode).trim().length > 0 && f.city && String(f.city).trim().length > 0 && this.phoneReady
5928
7090
  );
5929
7091
  }
5930
7092
  /** Whether a valid shipping option is selected (when shippings exist). */
@@ -5947,7 +7109,6 @@ var _VioCheckout = class _VioCheckout extends LitElement {
5947
7109
  if (Array.isArray(methods)) {
5948
7110
  this.availableMethods = methods.map((m) => m.name);
5949
7111
  this.stripeMode = await Vio.checkout.getStripeMode(spId);
5950
- this.vippsShipsInApp = this.availableMethods.some((m) => String(m).toLowerCase() === "vipps") ? await Vio.checkout.getVippsExpressEnabled(spId) : false;
5951
7112
  } else {
5952
7113
  this.availableMethods = null;
5953
7114
  }
@@ -6165,21 +7326,44 @@ var _VioCheckout = class _VioCheckout extends LitElement {
6165
7326
  });
6166
7327
  }
6167
7328
  /**
6168
- * Vipps from the checkout: Express when the channel allows it, so the
6169
- * shopper picks the delivery inside the app — our rate on the cart is the
6170
- * one preselected there — and never silently keeps the default our form
6171
- * chose for them. When the cart cannot have Express (several suppliers)
6172
- * the backend says so without creating a payment, and the plain payment
6173
- * follows: profile shared, the rate already on the cart.
7329
+ * Vipps from the checkout: the STANDARD payment, always (Mote & Livsstil,
7330
+ * 2026-10-28). The shopper filled our form — name, address, mobile — and
7331
+ * chose the delivery with its price here; the payment carries that address
7332
+ * and the rate on the cart, and Vipps only approves and pays. Express (the
7333
+ * app filling in name and address) is the product detail's «Kjøp nå med
7334
+ * Vipps» alone, even when the brand has Express on.
6174
7335
  */
6175
7336
  async startVippsFromCheckout() {
6176
7337
  const sponsorId = this.checkoutState?.sponsorId;
6177
- const express = await Vio.checkout.getVippsExpressEnabled(sponsorId);
6178
- if (express) {
6179
- const res = await Vio.checkout.startVippsPayment(sponsorId, this.form, { express: true });
6180
- if (res?.payment_url) return;
7338
+ await Vio.checkout.startVippsPayment(sponsorId, this.form, { express: false });
7339
+ }
7340
+ /**
7341
+ * The chosen delivery on the cart before a hosted payment is minted (its
7342
+ * total depends on it). False — and the payment must not start — when it
7343
+ * could not be saved: the selected rate has no supplier to save it under,
7344
+ * or the backend refused. A cart with no rates at all has nothing to save.
7345
+ */
7346
+ async persistSelectedShipping(method) {
7347
+ if (this.availableShippingsList.length === 0) return true;
7348
+ const selectedOpt = this.shippingList.find((s) => s.id === this.selectedShipping);
7349
+ if (!selectedOpt?.supplierId) {
7350
+ if (typeof console !== "undefined") {
7351
+ console.warn(`[VioCheckout] no supplier for the selected shipping \u2014 ${method} not started`);
7352
+ }
7353
+ return false;
7354
+ }
7355
+ try {
7356
+ const updated = await Vio.checkout.updateShippingsBySupplier(
7357
+ [{ shipping_id: selectedOpt.id, supplier_id: Number(selectedOpt.supplierId) }],
7358
+ this.checkoutState?.sponsorId
7359
+ );
7360
+ return updated != null;
7361
+ } catch (err) {
7362
+ if (typeof console !== "undefined") {
7363
+ console.warn(`[VioCheckout] updateShippingsBySupplier on ${method} pay failed:`, err);
7364
+ }
7365
+ return false;
6181
7366
  }
6182
- await Vio.checkout.startVippsPayment(sponsorId, this.form);
6183
7367
  }
6184
7368
  async onApplePay() {
6185
7369
  if (this.applePayInProgress) return;
@@ -6198,6 +7382,10 @@ var _VioCheckout = class _VioCheckout extends LitElement {
6198
7382
  if (this.shippingBlocked) return;
6199
7383
  this.paymentError = null;
6200
7384
  this.paymentNotice = null;
7385
+ if (!this.cartPayable) {
7386
+ this.paymentError = EMPTY_CART_MESSAGE2;
7387
+ return;
7388
+ }
6201
7389
  if (method === "apple-pay") {
6202
7390
  void this.onApplePay();
6203
7391
  return;
@@ -6218,8 +7406,9 @@ var _VioCheckout = class _VioCheckout extends LitElement {
6218
7406
  Vio.checkout.selectPaymentMethod("nexi");
6219
7407
  return;
6220
7408
  }
6221
- if (method !== "vipps") {
6222
- const missing = !this.isAddressValid ? "Vennligst fyll ut alle feltene i leveringsadresse." : !this.hasSelectedShipping ? "Vennligst velg en fraktmetode." : null;
7409
+ {
7410
+ const phoneMissing = method === "vipps" && !isValidPhone(this.form.phone, this.phoneCountry);
7411
+ const missing = !this.isAddressValid || phoneMissing ? "Vennligst fyll ut alle feltene i leveringsadresse." : !this.hasSelectedShipping ? "Vennligst velg en fraktmetode." : null;
6223
7412
  if (missing) {
6224
7413
  if (this.checkoutState?.paymentMethod !== method) {
6225
7414
  Vio.checkout.selectPaymentMethod(method);
@@ -6243,20 +7432,12 @@ var _VioCheckout = class _VioCheckout extends LitElement {
6243
7432
  this.stripeRequested = true;
6244
7433
  return;
6245
7434
  }
6246
- const selectedOpt = this.shippingList.find((s) => s.id === this.selectedShipping);
6247
- if (selectedOpt?.supplierId) {
6248
- try {
6249
- await Vio.checkout.updateShippingsBySupplier(
6250
- [{ shipping_id: selectedOpt.id, supplier_id: Number(selectedOpt.supplierId) }],
6251
- this.checkoutState?.sponsorId
6252
- );
6253
- } catch (err) {
6254
- if (typeof console !== "undefined") {
6255
- console.warn("[VioCheckout] updateShippingsBySupplier on stripe pay failed:", err);
6256
- }
6257
- }
6258
- }
6259
7435
  this.stripeLoading = true;
7436
+ if (!await this.persistSelectedShipping("stripe")) {
7437
+ this.stripeLoading = false;
7438
+ this.paymentError = SHIPPING_NOT_SAVED_MESSAGE;
7439
+ return;
7440
+ }
6260
7441
  void Vio.checkout.startStripePayment(this.checkoutState?.sponsorId, this.form).catch((err) => {
6261
7442
  if (typeof console !== "undefined") {
6262
7443
  console.warn("[VioCheckout] Stripe payment could not start:", err);
@@ -6268,6 +7449,11 @@ var _VioCheckout = class _VioCheckout extends LitElement {
6268
7449
  }
6269
7450
  if (method === "vipps") {
6270
7451
  this.vippsLoading = true;
7452
+ if (!await this.persistSelectedShipping("vipps")) {
7453
+ this.vippsLoading = false;
7454
+ this.paymentError = SHIPPING_NOT_SAVED_MESSAGE;
7455
+ return;
7456
+ }
6271
7457
  const redirect = beginPaymentRedirect({ method: "vipps", sponsorId: this.checkoutState?.sponsorId, source: "checkout" });
6272
7458
  void this.startVippsFromCheckout().catch((err) => {
6273
7459
  if (typeof console !== "undefined") {
@@ -6289,7 +7475,11 @@ var _VioCheckout = class _VioCheckout extends LitElement {
6289
7475
  const s = this.checkoutState;
6290
7476
  if (!s || !s.paymentMethod) return;
6291
7477
  if (this.shippingBlocked) return;
6292
- if (s.paymentMethod !== "vipps" && !this.isAddressValid) {
7478
+ if (!this.cartPayable) {
7479
+ this.paymentError = EMPTY_CART_MESSAGE2;
7480
+ return;
7481
+ }
7482
+ if (!this.isAddressValid) {
6293
7483
  this.paymentError = "Vennligst fyll ut alle feltene i leveringsadresse.";
6294
7484
  return;
6295
7485
  }
@@ -6514,6 +7704,14 @@ var _VioCheckout = class _VioCheckout extends LitElement {
6514
7704
  return method === "apple-pay" || method === "klarna" || isEmbeddedMethod(method ?? "") || // Adyen's Drop-in brings its own Pay button.
6515
7705
  isFormFirstWidgetMethod(method ?? "");
6516
7706
  }
7707
+ /**
7708
+ * A total above 0 — every pay button needs it. The state's subtotal follows
7709
+ * the cart (CheckoutManager.onCartChange): a cart emptied or paid in
7710
+ * another tab drops it to 0 here.
7711
+ */
7712
+ get cartPayable() {
7713
+ return (this.checkoutState?.subtotal ?? 0) > 0;
7714
+ }
6517
7715
  orderTotal() {
6518
7716
  const s = this.checkoutState;
6519
7717
  if (!s) return "";
@@ -6523,7 +7721,6 @@ var _VioCheckout = class _VioCheckout extends LitElement {
6523
7721
  * express always has one; the normal flow has one once the backend
6524
7722
  * shippings loaded (the choice IS persisted on the cart and charged). */
6525
7723
  shippingMajor() {
6526
- if (this.vippsChoosesShipping()) return 0;
6527
7724
  if (this.checkoutState?.paymentMethod === "qliro" && typeof this.qliroShipping?.price === "number") {
6528
7725
  return this.qliroShipping.price;
6529
7726
  }
@@ -6538,10 +7735,6 @@ var _VioCheckout = class _VioCheckout extends LitElement {
6538
7735
  if (!o) return 0;
6539
7736
  return o.priceMajor ?? o.price / 100;
6540
7737
  }
6541
- /** Vipps is the method and it runs as Express: the delivery is chosen in Vipps. */
6542
- vippsChoosesShipping() {
6543
- return this.checkoutState?.paymentMethod === "vipps" && this.vippsShipsInApp;
6544
- }
6545
7738
  /** The rate the summary names — Qliro's own pick while Qliro is the method. */
6546
7739
  summaryShippingName() {
6547
7740
  if (this.checkoutState?.paymentMethod === "qliro" && this.qliroShipping) {
@@ -7664,7 +8857,6 @@ var _VioCheckout = class _VioCheckout extends LitElement {
7664
8857
  renderCheckoutBody() {
7665
8858
  const method = this.checkoutState?.paymentMethod;
7666
8859
  const showCompleteCta = !!method && !this.isExpressMethod(method);
7667
- const isVipps = method === "vipps";
7668
8860
  const needsOurForm = !!method && !collectsOwnAddress(method);
7669
8861
  return html`
7670
8862
  ${needsOurForm ? html`
@@ -7703,6 +8895,22 @@ var _VioCheckout = class _VioCheckout extends LitElement {
7703
8895
  @input=${(e) => this.onFieldChange("email", e.target.value)}
7704
8896
  />
7705
8897
  </div>
8898
+ <div class="field">
8899
+ <label for="vio-checkout-phone">${this.phoneRequired ? "Mobilnummer *" : "Mobilnummer (valgfritt)"}</label>
8900
+ <input
8901
+ id="vio-checkout-phone"
8902
+ class="phone-input"
8903
+ type="tel"
8904
+ ?required=${this.phoneRequired}
8905
+ inputmode="tel"
8906
+ autocomplete="tel"
8907
+ placeholder=${this.phoneCountry.toUpperCase() === "NO" ? "8 siffer" : "+46 70 123 45 67"}
8908
+ aria-invalid=${this.phoneError ? "true" : "false"}
8909
+ .value=${this.form.phone ?? ""}
8910
+ @input=${(e) => this.onFieldChange("phone", e.target.value)}
8911
+ />
8912
+ ${this.phoneError ? html`<div class="field-error" role="alert">Skriv et gyldig mobilnummer (med landskode, f.eks. +46, hvis det ikke er norsk)</div>` : ""}
8913
+ </div>
7706
8914
  <div class="field">
7707
8915
  <label>Adresse *</label>
7708
8916
  <input
@@ -7767,22 +8975,6 @@ var _VioCheckout = class _VioCheckout extends LitElement {
7767
8975
  </section>
7768
8976
  ` : ""}
7769
8977
 
7770
- ${isVipps ? html`
7771
- <section class="section">
7772
- <div class="section-label">Kontakt</div>
7773
- <div class="field">
7774
- <label>E-post * <span style="font-weight:400;opacity:0.6">(for kvittering — adresse fylles ut i Vipps)</span></label>
7775
- <input
7776
- type="email"
7777
- required
7778
- autocomplete="email"
7779
- .value=${this.form.email}
7780
- @input=${(e) => this.onFieldChange("email", e.target.value)}
7781
- />
7782
- </div>
7783
- </section>
7784
- ` : ""}
7785
-
7786
8978
  <section class="section">
7787
8979
  <div class="section-label">
7788
8980
  ${needsOurForm ? "Steg 2" : "Betaling"}
@@ -7822,7 +9014,7 @@ var _VioCheckout = class _VioCheckout extends LitElement {
7822
9014
  <button
7823
9015
  class="payment-btn primary complete-cta"
7824
9016
  @click=${() => void this.onPay("stripe")}
7825
- ?disabled=${this.stripeLoading || !this.isAddressValid || !this.hasSelectedShipping}
9017
+ ?disabled=${this.stripeLoading || !this.cartPayable || !this.isAddressValid || !this.hasSelectedShipping}
7826
9018
  title=${!this.isAddressValid ? "Vennligst fyll ut leveringsadresse" : !this.hasSelectedShipping ? "Vennligst velg en fraktmetode" : ""}
7827
9019
  >
7828
9020
  ${this.stripeLoading ? "G\xE5r til Stripe\u2026" : `Betal ${this.payTotalLabel()} med Stripe`}
@@ -7832,7 +9024,8 @@ var _VioCheckout = class _VioCheckout extends LitElement {
7832
9024
  <button
7833
9025
  class="payment-btn primary complete-cta"
7834
9026
  @click=${() => void this.onPay("vipps")}
7835
- ?disabled=${this.vippsLoading}
9027
+ ?disabled=${this.vippsLoading || !this.cartPayable || !this.isAddressValid || !this.hasSelectedShipping}
9028
+ title=${!this.isAddressValid ? "Vennligst fyll ut leveringsadresse" : !this.hasSelectedShipping ? "Vennligst velg en fraktmetode" : ""}
7836
9029
  >
7837
9030
  ${this.vippsLoading ? "G\xE5r til Vipps\u2026" : `Betal ${this.payTotalLabel()} med Vipps`}
7838
9031
  </button>
@@ -7841,7 +9034,7 @@ var _VioCheckout = class _VioCheckout extends LitElement {
7841
9034
  <button
7842
9035
  class="payment-btn primary complete-cta"
7843
9036
  @click=${() => void this.onPay("apple-pay")}
7844
- ?disabled=${this.applePayInProgress || !this.isAddressValid || !this.hasSelectedShipping}
9037
+ ?disabled=${this.applePayInProgress || !this.cartPayable || !this.isAddressValid || !this.hasSelectedShipping}
7845
9038
  title=${!this.isAddressValid ? "Vennligst fyll ut leveringsadresse" : !this.hasSelectedShipping ? "Vennligst velg en fraktmetode" : ""}
7846
9039
  >
7847
9040
  ${this.applePayInProgress ? "\xC5pner\u2026" : `Betal ${this.payTotalLabel()} med Apple Pay`}
@@ -7851,7 +9044,7 @@ var _VioCheckout = class _VioCheckout extends LitElement {
7851
9044
  <button
7852
9045
  class="payment-btn primary complete-cta"
7853
9046
  @click=${this.onCompleteOrder}
7854
- ?disabled=${!this.isAddressValid || !this.hasSelectedShipping}
9047
+ ?disabled=${!this.cartPayable || !this.isAddressValid || !this.hasSelectedShipping}
7855
9048
  title=${!this.isAddressValid ? "Vennligst fyll ut leveringsadresse" : !this.hasSelectedShipping ? "Vennligst velg en fraktmetode" : ""}
7856
9049
  >
7857
9050
  Betal ${this.payTotalLabel()} med ${this.methodLabel(method ?? null)}
@@ -7964,7 +9157,7 @@ var _VioCheckout = class _VioCheckout extends LitElement {
7964
9157
  </div>
7965
9158
  <div class="order-row">
7966
9159
  <span>Frakt${this.shippingMajor() > 0 && this.summaryShippingName() ? ` \u2013 ${this.summaryShippingName()}` : ""}</span>
7967
- <span>${this.vippsChoosesShipping() ? "Velges i Vipps" : this.shippingMajor() > 0 ? formatPrice(this.shippingMajor(), this.checkoutState?.currency) : this.shippingLabel}</span>
9160
+ <span>${this.shippingMajor() > 0 ? formatPrice(this.shippingMajor(), this.checkoutState?.currency) : this.shippingLabel}</span>
7968
9161
  </div>
7969
9162
  <div class="order-row total">
7970
9163
  <span>Totalt</span><span>${this.payTotalLabel()}</span>
@@ -7985,9 +9178,6 @@ __decorateClass([
7985
9178
  __decorateClass([
7986
9179
  property({ type: String, attribute: "shipping-label" })
7987
9180
  ], _VioCheckout.prototype, "shippingLabel", 2);
7988
- __decorateClass([
7989
- state()
7990
- ], _VioCheckout.prototype, "vippsShipsInApp", 2);
7991
9181
  __decorateClass([
7992
9182
  property({ type: Boolean, reflect: true })
7993
9183
  ], _VioCheckout.prototype, "express", 2);
@@ -8163,5 +9353,5 @@ function registerVioElements() {
8163
9353
  }
8164
9354
 
8165
9355
  export { VioCart, VioCheckout, VioProduct, VioProductCarousel, VioProductDetail, VioTokens, applyVioTheme, registerVioElements };
8166
- //# sourceMappingURL=chunk-L6YYYKQK.js.map
8167
- //# sourceMappingURL=chunk-L6YYYKQK.js.map
9356
+ //# sourceMappingURL=chunk-C5Q6W4B2.js.map
9357
+ //# sourceMappingURL=chunk-C5Q6W4B2.js.map