PageSourceSearch

https://www.zitatdestages.net/app/sheet/app-sheet.js

js zitatdestages.net collected 2026-10-01 23:53:21 UTC 48,923 bytes, 1,645 lines download raw bytes

1class AppSheet extends HTMLElement {
2  constructor() {
3    super();
4    this.attachShadow({ mode: "open" });
5    this.currentIndex = 0;
6    this.isAndroid = /Android/i.test(navigator.userAgent);
7    this.language = "en";
8  }
9
10  static get observedAttributes() {
11    return ["language", "open", "mini", "asdialog", "variant"];
12  }
13
14  get isDialog() {
15    return this.hasAttribute("asdialog");
16  }
17
18  // "readmore" swaps the branding copy for a "continue reading in the app" pitch,
19  // reusing the exact same coverflow sheet (full + mini). Absent an explicit variant
20  // attribute, each element instance is randomly assigned one (50/50) the first time
21  // it's checked, and that choice sticks for its lifetime.
22  get variant() {
23    const attr = this.getAttribute("variant");
24    if (attr === "readmore" || attr === "sheet") return attr;
25    if (!this._resolvedVariant) {
26      this._resolvedVariant = Math.random() < 0.5 ? "readmore" : "sheet";
27    }
28    return this._resolvedVariant;
29  }
30
31  attributeChangedCallback(name, oldValue, newValue) {
32    if (name === "language" && newValue) {
33      this.language = newValue;
34      if (this.isConnected) {
35        if (this.isReadMore) {
36          // Readmore copy lives in the rendered markup, so swap it via full re-render
37          this.render();
38          this.loadScreenshots();
39          this.setupEventListeners();
40          this.updateVisibility();
41        } else {
42          this.loadScreenshots();
43        }
44      }
45    }
46    if (name === "open" || name === "mini") {
47      if (name === "mini" && oldValue !== newValue) {
48        // Mini mode changed - need to re-render to update text and button
49        this.render();
50        this.loadScreenshots();
51        this.setupEventListeners();
52      }
53      this.updateVisibility();
54    }
55    if (name === "variant" && this.isConnected) {
56      this.render();
57      this.loadScreenshots();
58      this.setupEventListeners();
59      this.updateVisibility();
60    }
61  }
62
63  get dialogSeen() {
64    return document.cookie.split("; ").some((cookie) => cookie.startsWith("promo_video_export_v1=1"));
65  }
66
67  connectedCallback() {
68    if (this.isDialog && !this.dialogSeen) {
69      this.renderDialog();
70      return;
71    }
72
73    this.render();
74    this.loadScreenshots();
75    this.setupEventListeners();
76    this.updateVisibility();
77  }
78
79  get isReadMore() {
80    return this.variant === "readmore";
81  }
82
83  // Store-button label; the readmore variant sells "keep reading", not "install".
84  get ctaText() {
85    if (this.isReadMore) {
86      return this.language === "de" ? "Kostenlos weiterlesen" : "Keep reading for free";
87    }
88    return this.language === "de" ? "Jetzt installieren" : "Install now";
89  }
90
91  get freeText() {
92    if (this.isReadMore) {
93      return this.language === "de" ? "App herunterladen, um weiterzulesen" : "Download the app to keep reading";
94    }
95    // Short enough to share one line with the stars on a 375px viewport
96    return this.language === "de" ? (this.isAndroid ? "Gratis für Android" : "Gratis für iPhone & iPad") : this.isAndroid ? "Free for Android" : "Free for iPhone & iPad";
97  }
98
99  render() {
100    const platform = this.isAndroid ? "Android" : "iOS";
101    const storeName = this.isAndroid ? "Google Play" : "App Store";
102    const appTitle = this.isReadMore
103      ? this.language === "de"
104        ? "Das Zitat des Tages ist besser in der App"
105        : "The Quote of the Day is better in the app"
106      : this.language === "de"
107        ? "A-Z Zitat des Tages"
108        : "Literally: Quotes and Thoughts";
109
110    let downloadText;
111    if (this.language === "de") {
112      downloadText = this.isAndroid ? "Jetzt kostenlos bei Google Play holen" : "Jetzt kostenlos im AppStore";
113    } else {
114      downloadText = this.isAndroid ? "Free on Google Play" : "Free for iPhone and iPad";
115    }
116
117    const isMini = this.hasAttribute("mini");
118
119    // In mini mode, show download text as subtitle
120    const appSubtitle = isMini
121      ? downloadText
122      : this.language === "de"
123        ? "Motivierende Zitate, positiven Affirmationen oder herzlichen Grüßen für besondere Anlässe? Mit dieser App haben Sie all das jederzeit auf Ihrem Handy"
124        : "Daily inspirational quotes, positive affirmations, and heartfelt greetings for special occasions - all at your fingertips";
125
126    const readmoreHeadline = this.language === "de" ? "Mehr Zitate. <em>Mehr Inspiration.</em> Jeden Tag." : "More quotes. <em>More inspiration.</em> Every day.";
127    const readmoreBody =
128      this.language === "de" ? "Die App zum Zitat des Tages – jeden Morgen frische Inspiration, direkt aufs Handy." : "The Quote of the Day app – fresh inspiration on your phone, every morning.";
129    const readmoreBenefits =
130      this.language === "de" ? ["Jeden Tag ein neues Zitat", "Komplett ohne Werbung", "Zitate als Video teilen"] : ["A new quote every day", "Completely ad-free", "Share quotes as videos"];
131    const checkIcon = '<svg viewBox="0 0 24 24">
131<path d="M9 16.17L4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41z"/></svg>';
132
133    const sheetContentBody = this.isReadMore
134      ? `
135          <div class="readmore-copy">
136            <div class="readmore-quotemark" aria-hidden="true">“</div>
137            <p class="readmore-headline">${readmoreHeadline}</p>
138            <p class="readmore-body">${readmoreBody}</p>
139            <ul class="readmore-benefits">
140              ${readmoreBenefits.map((b) => `<li><span class="check">${checkIcon}</span>${b}</li>`).join("")}
141            </ul>
142          </div>
143        `
144      : `
145          <div class="coverflow-container">
146            <button class="nav-button prev" aria-label="Previous">
147              <svg viewBox="0 0 24 24"><path d="M15.41 7.41L14 6l-6 6 6 6 1.41-1.41L10.83 12z"/></svg>
148            </button>
149            <div class="coverflow-stage"></div>
150            <button class="nav-button next" aria-label="Next">
151              <svg viewBox="0 0 24 24"><path d="M10 6L8.59 7.41 13.17 12l-4.58 4.59L10 18l6-6z"/></svg>
152            </button>
153          </div>
154
155          <div class="dots"></div>
156        `;
157
158    // Each variant loads its own display font (fallbacks: Georgia / system sans).
159    // readmore: Playfair Display for the editorial "quotes" look.
160    // sheet: Manrope for a clean, premium product look.
161    const fontImport = this.isReadMore
162      ? "@import url('https://fonts.googleapis.com/css2?family=Playfair+Display:ital,wght@0,600;0,700;0,800;1,600;1,700&display=swap');"
163      : "@import url('https://fonts.googleapis.com/css2?family=Manrope:wght@500;600;700;800&display=swap');";
164
165    this.shadowRoot.innerHTML = `
166      <style>
167        ${fontImport}
168        :host {
169          font-family: var(--sans);
170          --sans: 'Manrope', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
171          --serif: 'Playfair Display', 'Iowan Old Style', Georgia, 'Times New Roman', serif;
172          --accent-gradient: linear-gradient(120deg, #ffb547, #ff5f45);
173          --sheet-bg: #ffffff;
174          --text-primary: #1d1d1f;
175          --text-secondary: #86868b;
176          --backdrop-color: rgba(0, 0, 0, 0.4);
177        }
178
179        .backdrop {
180          position: fixed;
181          top: 0;
182          left: 0;
183          right: 0;
184          bottom: 0;
185          background: var(--backdrop-color);
186          z-index: 9998;
187          opacity: 0;
188          visibility: hidden;
189          transition: opacity 0.3s ease, visibility 0.3s ease;
190        }
191
192        .backdrop.show {
193          opacity: 1;
194          visibility: visible;
195        }
196
197        .sheet {
198          position: fixed;
199          bottom: 0;
200          left: 0;
201          right: 0;
202          background:
203            radial-gradient(120% 70% at 50% 0%, rgba(255, 255, 255, 0.9) 0%, rgba(255, 255, 255, 0) 60%),
204            linear-gradient(180deg, #fdfdfd 0%, #f2f2f4 100%);
205          border-radius: 24px 24px 0 0;
206          z-index: 9999;
207          max-width: 600px;
208          margin: 0 auto;
209          max-height: 90vh;
210          overflow: hidden;
211          transform: translateY(100%);
212          transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1);
213          box-shadow: 0 -8px 32px rgba(0, 0, 0, 0.16);
214        }
215
216        .sheet.show {
217          transform: translateY(0);
218        }
219
220        .sheet-header {
221          padding: 10px 16px 14px;
222          border-bottom: 1px solid rgba(0, 0, 0, 0.06);
223          position: relative;
224        }
225
226        .sheet-handle {
227          width: 36px;
228          height: 5px;
229          background: rgba(0, 0, 0, 0.16);
230          border-radius: 3px;
231          margin: 0 auto 12px;
232        }
233
234        .header-content {
235          display: flex;
236          align-items: center;
237          gap: 12px;
238        }
239
240        .header-link {
241          display: flex;
242          align-items: center;
243          gap: 12px;
244          text-decoration: none;
245          color: inherit;
246          flex: 1;
247          min-width: 0;
248          transition: opacity 0.2s;
249        }
250
251        .header-link:hover {
252          opacity: 0.7;
253        }
254
255        .app-icon {
256          width: 60px;
257          height: 60px;
258          border-radius: 14px;
259          box-shadow: 0 4px 14px rgba(0, 0, 0, 0.16);
260          flex-shrink: 0;
261        }
262
263        .header-text {
264          flex: 1;
265          min-width: 0;
266          text-align: left;
267          padding-right: 36px;
268        }
269
270        .header-title {
271          font-size: 21px;
272          font-weight: 800;
273          letter-spacing: -0.02em;
274          margin: 0 0 4px 0;
275          color: var(--text-primary);
276          white-space: nowrap;
277          overflow: hidden;
278          text-overflow: ellipsis;
279        }
280
281        .header-subtitle {
282          font-size: 13px;
283          color: var(--text-secondary);
284          margin: 0;
285          line-height: 1.4;
286        }
287
288        .rating {
289          display: flex;
290          align-items: center;
291          gap: 4px;
292          margin-top: 4px;
293          min-width: 0;
294        }
295
296        .rating-stars {
297          display: flex;
298          gap: 2px;
299        }
300
301        .rating-stars svg {
302          width: 14px;
303          height: 14px;
304          fill: #ffb800;
305        }
306
307        .rating-text {
308          font-size: 13px;
309          font-weight: 600;
310          color: var(--text-secondary);
311        }
312
313        .rating-separator {
314          font-size: 13px;
315          color: var(--text-secondary);
316          margin: 0 2px;
317        }
318
319        .free-text {
320          font-size: 13px;
321          font-weight: 600;
322          color: var(--text-secondary);
323          white-space: nowrap;
324          overflow: hidden;
325          text-overflow: ellipsis;
326        }
327
328        .rating-stars,
329        .rating-text,
330        .rating-separator {
331          flex-shrink: 0;
332        }
333
334        .close-button {
335          position: absolute;
336          top: 16px;
337          right: 16px;
338          width: 32px;
339          height: 32px;
340          border-radius: 50%;
341          background: rgba(0, 0, 0, 0.06);
342          border: none;
343          cursor: pointer;
344          display: flex;
345          align-items: center;
346          justify-content: center;
347          transition: background 0.2s;
348          padding: 0;
349        }
350
351        .close-button:hover {
352          background: rgba(0, 0, 0, 0.12);
353        }
354
355        .close-button svg {
356          width: 16px;
357          height: 16px;
358          fill: #77777c;
359        }
360
361        /* With the drag handle above, the close button sits level with the title row */
362        .sheet:not(.mini):not(.readmore) .close-button {
363          top: 30px;
364        }
365
366        .sheet-content {
367          padding: 0 20px 24px;
368          overflow-y: auto;
369          max-height: calc(90vh - 60px);
370          scrollbar-width: none;
371          -ms-overflow-style: none;
372        }
373
374        .sheet-content::-webkit-scrollbar {
375          display: none;
376        }
377
378
379
380        .coverflow-container {
381          position: relative;
382          width: calc(100% + 40px);
383          height: 420px;
384          perspective: 1200px;
385          margin: 0 -20px 24px -20px;
386          overflow: hidden;
387        }
388
389        .coverflow-stage {
390          position: relative;
391          width: 100%;
392          height: 100%;
393          transform-style: preserve-3d;
394          transition: transform 0.4s cubic-bezier(0.4, 0, 0.2, 1);
395        }
396
397        .screenshot {
398          position: absolute;
399          left: 50%;
400          top: 50%;
401          width: 225px;
402          height: 400px;
403          margin-left: -112.5px;
404          margin-top: -200px;
405          border-radius: 20px;
406          overflow: hidden;
407          box-shadow: 0 10px 40px rgba(0, 0, 0, 0.3);
408          transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1);
409          cursor: pointer;
410          background: #2a2a2a;
411        }
412
413        /* iOS screenshots have different aspect ratio (236:512) */
414        .screenshot.ios {
415          width: 184px;
416          margin-left: -92px;
417        }
418
419        .screenshot img {
420          width: 100%;
421          height: 100%;
422          object-fit: contain;
423          user-select: none;
424          -webkit-user-drag: none;
425        }
426
427        .nav-button {
428          position: absolute;
429          top: 50%;
430          transform: translateY(-50%);
431          background: rgba(255, 255, 255, 0.92);
432          border: none;
433          width: 40px;
434          height: 40px;
435          border-radius: 50%;
436          cursor: pointer;
437          display: flex;
438          align-items: center;
439          justify-content: center;
440          z-index: 10;
441          box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
442          transition: background 0.2s;
443        }
444
445        .nav-button:hover {
446          background: rgba(255, 255, 255, 1);
447        }
448
449        .nav-button.prev {
450          left: 20px;
451        }
452
453        .nav-button.next {
454          right: 20px;
455        }
456
457        .nav-button svg {
458          width: 20px;
459          height: 20px;
460          fill: #000;
461        }
462
463        .store-section {
464          text-align: center;
465          padding: 20px 0;
466        }
467
468        .store-button {
469          display: inline-flex;
470          align-items: center;
471          gap: 12px;
472          padding: 14px 30px;
473          background: linear-gradient(180deg, #2c2c2e 0%, #111113 100%);
474          color: #fff;
475          text-decoration: none;
476          border-radius: 14px;
477          font-size: 16px;
478          font-weight: 700;
479          letter-spacing: 0.01em;
480          box-shadow: 0 8px 22px rgba(0, 0, 0, 0.22);
481          transition: transform 0.2s, opacity 0.2s;
482        }
483
484        .store-button:hover {
485          transform: translateY(-2px);
486          opacity: 0.9;
487        }
488
489        .store-button-lg {
490          display: flex;
491          box-sizing: border-box;
492          width: 100%;
493          justify-content: center;
494          padding: 18px 24px;
495          font-size: 19px;
496          font-weight: 800;
497          border-radius: 14px;
498          box-shadow: 0 8px 20px rgba(0, 0, 0, 0.2);
499        }
500
501        /* ===== Read-more variant: dark editorial look ===== */
502        .sheet.readmore {
503          cursor: pointer;
504          --text-primary: #f7f3e9;
505          --text-secondary: #b8b2a4;
506          background:
507            radial-gradient(130% 100% at 50% 0%, rgba(255, 149, 0, 0.18) 0%, rgba(255, 59, 48, 0.06) 34%, rgba(0, 0, 0, 0) 62%),
508            linear-gradient(180deg, #1d1a24 0%, #131017 55%, #0d0c11 100%);
509          box-shadow: 0 -10px 40px rgba(0, 0, 0, 0.45);
510        }
511
512        .sheet.readmore .sheet-header {
513          border-bottom-color: rgba(255, 255, 255, 0.08);
514        }
515
516        .sheet.readmore .sheet-content {
517          scrollbar-width: none;
518          -ms-overflow-style: none;
519        }
520
521        .sheet.readmore .sheet-content::-webkit-scrollbar {
522          display: none;
523        }
524
525        .sheet.readmore .header-title {
526          font-family: var(--serif);
527          font-size: 19px;
528          font-weight: 700;
529          letter-spacing: 0.01em;
530        }
531
532        .sheet.readmore .close-button {
533          background: rgba(255, 255, 255, 0.1);
534        }
535
536        .sheet.readmore .close-button:hover {
537          background: rgba(255, 255, 255, 0.18);
538        }
539
540        .sheet.readmore .close-button svg {
541          fill: #c9c4b8;
542        }
543
544        .readmore-copy {
545          padding: 14px 8px 4px;
546          text-align: center;
547        }
548
549        .readmore-quotemark {
550          font-family: var(--serif);
551          font-size: 96px;
552          font-weight: 700;
553          line-height: 1;
554          height: 54px;
555          background: var(--accent-gradient);
556          -webkit-background-clip: text;
557          background-clip: text;
558          -webkit-text-fill-color: transparent;
559          color: transparent;
560          user-select: none;
561        }
562
563        .readmore-headline {
564          margin: 0 0 14px;
565          font-family: var(--serif);
566          font-size: 34px;
567          font-weight: 700;
568          line-height: 1.18;
569          letter-spacing: 0;
570          color: var(--text-primary);
571        }
572
573        .readmore-headline em {
574          font-style: italic;
575          background: var(--accent-gradient);
576          -webkit-background-clip: text;
577          background-clip: text;
578          -webkit-text-fill-color: transparent;
579          color: transparent;
580        }
581
582        .readmore-body {
583          margin: 0 auto;
584          max-width: 42ch;
585          font-size: 16px;
586          font-weight: 400;
587          line-height: 1.55;
588          color: var(--text-secondary);
589        }
590
591        .readmore-benefits {
592          list-style: none;
593          margin: 22px auto 0;
594          padding: 0;
595          display: inline-flex;
596          flex-direction: column;
597          align-items: flex-start;
598          gap: 12px;
599        }
600
601        .readmore-benefits li {
602          display: flex;
603          align-items: center;
604          gap: 10px;
605          font-size: 15px;
606          font-weight: 500;
607          color: #ddd8cb;
608        }
609
610        .readmore-benefits .check {
611          width: 22px;
612          height: 22px;
613          border-radius: 50%;
614          background: var(--accent-gradient);
615          display: flex;
616          align-items: center;
617          justify-content: center;
618          flex-shrink: 0;
619        }
620
621        .readmore-benefits .check svg {
622          width: 13px;
623          height: 13px;
624          fill: #fff;
625        }
626
627        .sheet.readmore .store-button {
628          background: linear-gradient(90deg, #ff9500, #ff3b30);
629          border-radius: 16px;
630          box-shadow: 0 10px 28px rgba(255, 96, 48, 0.35);
631        }
632
633        .sheet.readmore .store-button:hover {
634          opacity: 1;
635        }
636
637        .sheet.readmore .more-info-link {
638          color: #938e82;
639        }
640
641        .sheet.readmore .more-info-link:hover {
642          color: var(--text-primary);
643        }
644
645        .store-button.google-play {
646          background: linear-gradient(180deg, #3a3a3c 0%, #1d1d1f 100%);
647        }
648
649        .store-icon {
650          width: 24px;
651          height: 24px;
652        }
653
654        .more-info-link {
655          display: inline-block;
656          margin-top: 12px;
657          color: var(--text-secondary);
658          text-decoration: none;
659          font-size: 14px;
660          transition: color 0.2s;
661        }
662
663        .more-info-link:hover {
664          color: var(--text-primary);
665          text-decoration: underline;
666        }
667
668        .dots {
669          display: flex;
670          justify-content: center;
671          gap: 8px;
672          margin-top: 16px;
673        }
674
675        .dot {
676          width: 8px;
677          height: 8px;
678          border-radius: 50%;
679          background: rgba(0, 0, 0, 0.14);
680          transition: background 0.3s, transform 0.3s;
681        }
682
683        .dot.active {
684          background: #1d1d1f;
685          transform: scale(1.2);
686        }
687
688        /* Mini mode styles */
689        .sheet.mini {
690          max-height: 74px;
691        }
692
693        .sheet.mini .sheet-content {
694          display: none;
695        }
696
697        .sheet.mini .sheet-header {
698          border-bottom: none;
699          cursor: pointer;
700          padding: 13px 20px;
701        }
702
703        .sheet.mini .header-subtitle {
704          font-weight: 500;
705        }
706
707        /* Sheet variant mini bar: everything one notch smaller so the
708           74px bar holds icon + title + rating without clipping */
709        .sheet.mini .app-icon {
710          width: 48px;
711          height: 48px;
712          border-radius: 11px;
713        }
714
715        .sheet.mini .header-title {
716          font-size: 17px;
717          margin: 0 0 2px 0;
718          white-space: nowrap;
719          overflow: hidden;
720          text-overflow: ellipsis;
721        }
722
723        .sheet.mini .header-text {
724          padding-right: 36px;
725        }
726
727        .sheet.mini .rating {
728          margin-top: 0;
729          min-width: 0;
730        }
731
732        .sheet.mini .free-text {
733          white-space: nowrap;
734          overflow: hidden;
735          text-overflow: ellipsis;
736        }
737
738        /* Readmore full mode: no header row at all — just the floating close button */
739        .sheet.readmore .sheet-header {
740          border-bottom: none;
741          padding: 0;
742        }
743
744        .sheet.readmore .readmore-mini-pitch {
745          display: none;
746        }
747
748        /* Readmore mini mode: clean serif pitch bar instead of icon + rating */
749        .sheet.mini.readmore .sheet-header {
750          padding: 13px 20px;
751        }
752
753        .sheet.mini.readmore .readmore-mini-pitch {
754          display: flex;
755          align-items: center;
756          gap: 12px;
757          padding-right: 44px;
758        }
759
760        .mini-logo {
761          width: 48px;
762          height: 48px;
763          border-radius: 11px;
764          box-shadow: 0 2px 8px rgba(0, 0, 0, 0.35);
765          flex-shrink: 0;
766        }
767
768        .mini-lines {
769          display: flex;
770          flex-direction: column;
771          gap: 2px;
772          min-width: 0;
773        }
774
775        .mini-text {
776          font-family: var(--serif);
777          font-size: 16px;
778          font-weight: 700;
779          letter-spacing: 0.01em;
780          color: var(--text-primary);
781          white-space: nowrap;
782          overflow: hidden;
783          text-overflow: ellipsis;
784        }
785
786        .mini-text .accent {
787          background: var(--accent-gradient);
788          -webkit-background-clip: text;
789          background-clip: text;
790          -webkit-text-fill-color: transparent;
791          color: transparent;
792        }
793
794        .mini-sub {
795          font-size: 13px;
796          font-weight: 600;
797          background: var(--accent-gradient);
798          -webkit-background-clip: text;
799          background-clip: text;
800          -webkit-text-fill-color: transparent;
801          color: transparent;
802          white-space: nowrap;
803          overflow: hidden;
804          text-overflow: ellipsis;
805        }
806
807        /* Hide backdrop in mini mode */
808        .backdrop.mini {
809          display: none;
810          opacity: 0;
811          visibility: hidden;
812          pointer-events: none;
813        }
814
815        @media (max-width: 768px) {
816          .sheet {
817            max-height: 75vh;
818            border-radius: 24px 24px 0 0;
819          }
820
821          .sheet-header {
822            padding: 8px 16px 12px;
823          }
824
825          .sheet-content {
826            padding: 0px 16px;
827            max-height: calc(75vh - 100px);
828            padding-bottom: 0;
829            overflow-y: auto;
830            scrollbar-width: none; /* Firefox */
831            -ms-overflow-style: none; /* IE and Edge */
832          }
833
834          .sheet-content::-webkit-scrollbar {
835            display: none; /* Chrome, Safari, Opera */
836          }
837
838          .cta-section {
839            padding: 12px 0;
840          }
841
842          .store-section {
843            padding: 20px 0 0 0;
844          }
845
846          .store-button {
847            padding: 10px 20px;
848            font-size: 15px;
849          }
850
851          .more-info-link {
852            margin-top: 8px;
853            font-size: 13px;
854          }
855
856          .dots {
857            margin-top: 12px;
858            gap: 6px;
859          }
860
861          .screenshot {
862            width: 135px;
863            height: 240px;
864            margin-left: -67.5px;
865            margin-top: -120px;
866          }
867
868          /* iOS screenshots on mobile */
869          .screenshot.ios {
870            width: 111px;
871            margin-left: -55.5px;
872          }
873
874          .coverflow-container {
875            height: 270px;
876            margin-bottom: 16px;
877            width: calc(100% + 32px);
878            margin-left: -16px;
879            margin-right: -16px;
880          }
881
882          .nav-button {
883            width: 36px;
884            height: 36px;
885          }
886
887          .nav-button.prev {
888            left: 10px;
889          }
890
891          .nav-button.next {
892            right: 10px;
893          }
894
895          .readmore-copy {
896            padding-top: 8px;
897          }
898
899          .readmore-quotemark {
900            font-size: 68px;
901            height: 38px;
902          }
903
904          .readmore-headline {
905            font-size: 26px;
906          }
907
908          .readmore-body {
909            font-size: 15px;
910          }
911
912          .readmore-benefits {
913            margin-top: 16px;
914            gap: 10px;
915          }
916
917          .readmore-benefits li {
918            font-size: 14px;
919          }
920
921          .sheet.readmore .store-section {
922            padding-bottom: max(16px, env(safe-area-inset-bottom));
923          }
924
925          .mini-logo {
926            width: 44px;
927            height: 44px;
928            border-radius: 10px;
929          }
930
931          .sheet.mini.readmore .readmore-mini-pitch {
932            gap: 10px;
933          }
934
935          .mini-text {
936            font-size: 14px;
937          }
938
939          .mini-sub {
940            font-size: 12px;
941          }
942        }
943      </style>
944
945      <div class="backdrop"></div>
946      <div class="sheet${this.isReadMore ? " readmore" : ""}">
947        <div class="sheet-header">
948          ${
949            this.isReadMore
950              ? `
951          <div class="readmore-mini-pitch">
952            <img class="mini-logo" src="${"https://www.zitatdestages.net/app/sheet/icon/Icon-" + this.language + ".png"}" alt="App Icon">
953            <div class="mini-lines">
954              <span class="mini-text">${this.language === "de" ? '<span class="accent">Aufgepasst:</span> Beliebte Zitate-App' : '<span class="accent">Heads up:</span> Popular quotes app'}</span>
955              <span class="mini-sub">${this.language === "de" ? "Nur mehr kurz kostenlos" : "Free for a short time only"}</span>
956            </div>
957          </div>
958          `
959              : `
960          ${isMini ? "" : '<div class="sheet-handle"></div>'}
961          <div class="header-content">
962            <a href="#" class="header-link" target="_blank" rel="noopener">
963              <img class="app-icon" src="${"https://www.zitatdestages.net/app/sheet/icon/Icon-" + this.language + ".png"}" alt="App Icon">
964              <div class="header-text">
965                <h1 class="header-title">${appTitle}</h1>
966                <div class="rating">
967                  <div class="rating-stars">
968                    <svg viewBox="0 0 24 24"><path d="M12 17.27L18.18 21l-1.64-7.03L22 9.24l-7.19-.61L12 2 9.19 8.63 2 9.24l5.46 4.73L5.82 21z"/></svg>
969                    <svg viewBox="0 0 24 24"><path d="M12 17.27L18.18 21l-1.64-7.03L22 9.24l-7.19-.61L12 2 9.19 8.63 2 9.24l5.46 4.73L5.82 21z"/></svg>
970                    <svg viewBox="0 0 24 24"><path d="M12 17.27L18.18 21l-1.64-7.03L22 9.24l-7.19-.61L12 2 9.19 8.63 2 9.24l5.46 4.73L5.82 21z"/></svg>
971                    <svg viewBox="0 0 24 24"><path d="M12 17.27L18.18 21l-1.64-7.03L22 9.24l-7.19-.61L12 2 9.19 8.63 2 9.24l5.46 4.73L5.82 21z"/></svg>
972                    <svg viewBox="0 0 24 24"><path d="M12 17.27L18.18 21l-1.64-7.03L22 9.24l-7.19-.61L12 2 9.19 8.63 2 9.24l5.46 4.73L5.82 21z" opacity="0.9"/></svg>
973                  </div>
974                  <span class="rating-text">${this.language === "de" ? "4,9" : "4.9"}</span>
975                  <span class="rating-separator">•</span>
976                  <span class="free-text">${this.freeText}</span>
977                </div>
978              </div>
979            </a>
980          </div>
981          `
982          }
983          <button class="close-button" aria-label="${isMini ? "Expand" : "Minimize"}">
984            ${
985              isMini
986                ? '<svg viewBox="0 0 24 24"><path d="M7.41 15.41L12 10.83l4.59 4.58L18 14l-6-6-6 6z"/></svg>'
987                : '<svg viewBox="0 0 24 24"><path d="M7.41 8.59L12 13.17l4.59-4.58L18 10l-6 6-6-6z"/></svg>'
988            }
989          </button>
990        </div>
991        <div class="sheet-content">
992          ${sheetContentBody}
993
994          <div class="store-section">
995            <a href="#" class="store-button ${this.isReadMore ? "store-button-lg" : ""} ${this.isAndroid ? "google-play" : ""}" target="_blank" rel="noopener">
996              ${this.isAndroid ? this.getGooglePlayIcon() : this.getAppStoreIcon()}
997              <span>${this.isReadMore ? this.ctaText : downloadText}</span>
998            </a>
999            <br>
1000            <a href="#" class="more-info-link" target="_blank" rel="noopener">
1001              ${this.language === "de" ? "Mehr Infos" : "More Information"}
1002            </a>
1003          </div>
1004        </div>
1005      </div>
1006    `;
1007  }
1008
1009  getAppStoreIcon() {
1010    return `<svg class="store-icon" viewBox="0 0 24 24" fill="white">
1011      <path d="M18.71 19.5c-.83 1.24-1.71 2.45-3.05 2.47-1.34.03-1.77-.79-3.29-.79-1.53 0-2 .77-3.27.82-1.31.05-2.3-1.32-3.14-2.53C4.25 17 2.94 12.45 4.7 9.39c.87-1.52 2.43-2.48 4.12-2.51 1.28-.02 2.5.87 3.29.87.78 0 2.26-1.07 3.81-.91.65.03 2.47.26 3.64 1.98-.09.06-2.17 1.28-2.15 3.81.03 3.02 2.65 4.03 2.68 4.04-.03.07-.42 1.44-1.38 2.83M13 3.5c.73-.83 1.94-1.46 2.94-1.5.13 1.17-.34 2.35-1.04 3.19-.69.85-1.83 1.51-2.95 1.42-.15-1.15.41-2.35 1.05-3.11z"/>
1012    </svg>`;
1013  }
1014
1015  getGooglePlayIcon() {
1016    return `<svg class="store-icon" viewBox="0 0 24 24" fill="white">
1017      <path d="M3,20.5V3.5C3,2.91 3.34,2.39 3.84,2.15L13.69,12L3.84,21.85C3.34,21.6 3,21.09 3,20.5M16.81,15.12L6.05,21.34L14.54,12.85L16.81,15.12M20.16,10.81C20.5,11.08 20.75,11.5 20.75,12C20.75,12.5 20.53,12.9 20.18,13.18L17.89,14.5L15.39,12L17.89,9.5L20.16,10.81M6.05,2.66L16.81,8.88L14.54,11.15L6.05,2.66Z"/>
1018    </svg>`;
1019  }
1020
1021  loadScreenshots() {
1022    const platform = this.isAndroid ? "Android" : "iOS";
1023    const stage = this.shadowRoot.querySelector(".coverflow-stage");
1024    const dotsContainer = this.shadowRoot.querySelector(".dots");
1025
1026    // Update app icon and title
1027    const appIcon = this.shadowRoot.querySelector(".app-icon");
1028    const headerTitle = this.shadowRoot.querySelector(".header-title");
1029    const storeButton = this.shadowRoot.querySelector(".store-button span");
1030    const ratingText = this.shadowRoot.querySelector(".rating-text");
1031    const freeText = this.shadowRoot.querySelector(".free-text");
1032
1033    if (appIcon) {
1034      appIcon.src = "https://www.zitatdestages.net/app/sheet/icon/Icon-" + this.language + ".png";
1035    }
1036    if (headerTitle) {
1037      headerTitle.textContent = this.isReadMore
1038        ? this.language === "de"
1039          ? "Das Zitat des Tages ist besser in der App"
1040          : "The Quote of the Day is better in the app"
1041        : this.language === "de"
1042          ? "A-Z Zitat des Tages"
1043          : "Literally: Quotes and Thoughts";
1044    }
1045    if (ratingText) {
1046      ratingText.textContent = this.language === "de" ? "4,9" : "4.9";
1047    }
1048    if (freeText) {
1049      freeText.textContent = this.freeText;
1050    }
1051    if (storeButton) {
1052      storeButton.textContent = this.ctaText;
1053    }
1054
1055    // Update more info link text
1056    const moreInfoLink = this.shadowRoot.querySelector(".more-info-link");
1057    if (moreInfoLink) {
1058      moreInfoLink.textContent = this.language === "de" ? "Mehr Infos" : "More Information";
1059    }
1060
1061    // Update store links when language changes
1062    this.updateStoreLinks();
1063
1064    // Only load screenshots if stage and dots exist (not in mini mode)
1065    if (!stage || !dotsContainer) return;
1066
1067    stage.innerHTML = "";
1068    dotsContainer.innerHTML = "";
1069
1070    for (let i = 1; i <= 10; i++) {
1071      const screenshot = document.createElement("div");
1072      screenshot.className = "screenshot";
1073      if (!this.isAndroid) {
1074        screenshot.classList.add("ios");
1075      }
1076      screenshot.dataset.index = i - 1;
1077
1078      const img = document.createElement("img");
1079      img.src = "https://www.zitatdestages.net/app/sheet/images/" + platform 
1079+ "/" + this.language + "/" + String(i).padStart(2, "0") + ".jpeg";
1080      img.alt = "Screenshot " + i;
1081      img.loading = "lazy";
1082
1083      screenshot.appendChild(img);
1084
1085      // Make screenshot clickable - go to store
1086      screenshot.addEventListener("click", (e) => {
1087        e.stopPropagation();
1088        const storeUrl = this.getStoreUrl();
1089        window.open(storeUrl, "_blank", "noopener,noreferrer");
1090      });
1091
1092      stage.appendChild(screenshot);
1093
1094      // Create dot
1095      const dot = document.createElement("div");
1096      dot.className = "dot";
1097      if (i === 1) dot.classList.add("active");
1098      dot.dataset.index = i - 1;
1099      dotsContainer.appendChild(dot);
1100    }
1101
1102    this.updateCarousel();
1103  }
1104
1105  setupEventListeners() {
1106    if (this.isDialog && !this.dialogSeen) {
1107      this.shadowRoot.querySelector(".close")?.addEventListener("click", () => {
1108        this.removeAttribute("open");
1109      });
1110
1111      this.shadowRoot.querySelector(".backdrop")?.addEventListener("click", (e) => {
1112        if (e.target.classList.contains("backdrop")) {
1113          this.removeAttribute("open");
1114        }
1115      });
1116
1117      return;
1118    }
1119
1120    const backdrop = this.shadowRoot.querySelector(".backdrop");
1121    const closeBtn = this.shadowRoot.querySelector(".close-button");
1122    const headerLink = this.shadowRoot.querySelector(".header-link");
1123    const sheetHeader = this.shadowRoot.querySelector(".sheet-header");
1124    const prevBtn = this.shadowRoot.querySelector(".nav-button.prev");
1125    const nextBtn = this.shadowRoot.querySelector(".nav-button.next");
1126    const stage = this.shadowRoot.querySelector(".coverflow-stage");
1127    const storeButton = this.shadowRoot.querySelector(".store-button");
1128
1129    backdrop.addEventListener("click", () => {
1130      // Backdrop only exists in full mode, so always switch to mini
1131      this.switchToMini();
1132    });
1133
1134    closeBtn.addEventListener("click", () => {
1135      if (this.hasAttribute("mini")) {
1136        this.expandFromMini();
1137      } else {
1138        this.switchToMini();
1139      }
1140    });
1141
1142    if (prevBtn) prevBtn.addEventListener("click", () => this.navigate(-1));
1143    if (nextBtn) nextBtn.addEventListener("click", () => this.navigate(1));
1144
1145    // Mini mode: swipe up or click header to expand
1146    // Full mode: swipe down on header to switch to mini
1147    if (sheetHeader) {
1148      let startY = 0;
1149      sheetHeader.addEventListener("touchstart", (e) => {
1150        startY = e.touches[0].clientY;
1151      });
1152      sheetHeader.addEventListener("touchend", (e) => {
1153        const endY = e.changedTouches[0].clientY;
1154        const diffY = startY - endY;
1155
1156        if (this.hasAttribute("mini")) {
1157          // Swipe up to expand from mini mode
1158          if (diffY > 40) {
1159            this.expandFromMini();
1160          }
1161        } else {
1162          // Swipe down to switch to mini mode
1163          if (diffY < -40) {
1164            this.switchToMini();
1165          }
1166        }
1167      });
1168      sheetHeader.addEventListener("click", (e) => {
1169        // Readmore: taps go to the store (handled by the sheet-wide listener below)
1170        if (!this.isReadMore && this.hasAttribute("mini") && !e.target.closest(".close-button")) {
1171          this.expandFromMini();
1172        }
1173      });
1174    }
1175
1176    // Readmore: the entire sheet is one big store link — any tap opens the store,
1177    // except the chevron button (minimize/expand) and real links (they navigate themselves)
1178    const sheetEl = this.shadowRoot.querySelector(".sheet");
1179    if (this.isReadMore && sheetEl) {
1180      sheetEl.addEventListener("click", (e) => {
1181        if (e.target.closest(".close-button") || e.target.closest("a")) return;
1182        window.open(this.getStoreUrl(), "_blank", "noopener,noreferrer");
1183      });
1184    }
1185
1186    // Touch/swipe support for carousel
1187    let startX = 0;
1188    let isDragging = false;
1189
1190    if (stage) {
1191      stage.addEventListener("touchstart", (e) => {
1192        startX = e.touches[0].clientX;
1193        isDragging = true;
1194      });
1195
1196      stage.addEventListener(
1197        "touchmove",
1198        (e) => {
1199          if (!isDragging) return;
1200          e.preventDefault();
1201        },
1202        { passive: false },
1203      );
1204
1205      stage.addEventListener("touchend", (e) => {
1206        if (!isDragging) return;
1207        const endX = e.changedTouches[0].clientX;
1208        const diff = startX - endX;
1209
1210        if (Math.abs(diff) > 50) {
1211          this.navigate(diff > 0 ? 1 : -1);
1212        }
1213        isDragging = false;
1214      });
1215
1216      // Mouse drag support
1217      stage.addEventListener("mousedown", (e) => {
1218        startX = e.clientX;
1219        isDragging = true;
1220      });
1221
1222      stage.addEventListener("mousemove", (e) => {
1223        if (!isDragging) return;
1224        e.preventDefault();
1225      });
1226
1227      stage.addEventListener("mouseup", (e) => {
1228        if (!isDragging) return;
1229        const endX = e.clientX;
1230        const diff = startX - endX;
1231
1232        if (Math.abs(diff) > 50) {
1233          this.navigate(diff > 0 ? 1 : -1);
1234        }
1235        isDragging = false;
1236      });
1237
1238      stage.addEventListener("mouseleave", () => {
1239        isDragging = false;
1240      });
1241    }
1242
1243    // Dot click navigation
1244    const dotsContainer = this.shadowRoot.querySelector(".dots");
1245    if (dotsContainer) {
1246      dotsContainer.addEventListener("click", (e) => {
1247        if (e.target.classList.contains("dot")) {
1248          this.currentIndex = parseInt(e.target.dataset.index);
1249          this.updateCarousel();
1250        }
1251      });
1252    }
1253
1254    // Mouse wheel scrolling
1255    const container = this.shadowRoot.querySelector(".coverflow-container");
1256    if (container) {
1257      container.addEventListener(
1258        "wheel",
1259        (e) => {
1260          e.preventDefault();
1261          if (Math.abs(e.deltaY) > 10) {
1262            this.navigate(e.deltaY > 0 ? 1 : -1);
1263          }
1264        },
1265        { passive: false },
1266      );
1267    }
1268
1269    // Keyboard navigation (registered once on the host; setupEventListeners
1270    // can be called repeatedly on re-render, so guard against stacking listeners)
1271    if (!this._keydownHandler) {
1272      this._keydownHandler = (e) => {
1273        if (e.key === "ArrowLeft") {
1274          this.navigate(-1);
1275        } else if (e.key === "ArrowRight") {
1276          this.navigate(1);
1277        }
1278      };
1279      this.addEventListener("keydown", this._keydownHandler);
1280    }
1281
1282    // Update store links
1283    this.updateStoreLinks();
1284  }
1285
1286  expandFromMini() {
1287    // Simply remove the mini attribute - the reactive system will handle the rest
1288    this.removeAttribute("mini");
1289  }
1290
1291  switchToMini() {
1292    // Switch from full mode to mini mode
1293    this.setAttribute("mini", "");
1294    this.setAttribute("open", "");
1295  }
1296
1297  getStoreUrl() {
1298    if (this.language === "de") {
1299      // German app: "A-Z Zitat des Tages"
1300      if (this.isAndroid) {
1301        return "https://play.google.com/store/apps/details?id=at.chweb.zitatdestages";
1302      } else {
1303        return "https://apps.apple.com/app/zitat-des-tages-2025/id6464222313";
1304      }
1305    } else {
1306      // English app: "Literally: Quotes and Thoughts"
1307      if (this.isAndroid) {
1308        return "https://play.google.com/store/apps/details?id=literally.cc";
1309      } else {
1310        return "https://apps.apple.com/app/literally-quotes-and-thoughts/id6504204090";
1311      }
1312    }
1313  }
1314
1315  updateStoreLinks() {
1316    const storeButton = this.shadowRoot.querySelector(".store-button");
1317    const headerLink = this.shadowRoot.querySelector(".header-link");
1318    const moreInfoLink = this.shadowRoot.querySelector(".more-info-link");
1319    const storeUrl = this.getStoreUrl();
1320
1321    if (storeButton) {
1322      storeButton.href = storeUrl;
1323    }
1324    if (headerLink) {
1325      headerLink.href = storeUrl;
1326    }
1327    if (moreInfoLink) {
1328      moreInfoLink.href = storeUrl;
1329    }
1330  }
1331
1332  navigate(direction) {
1333    this.currentIndex += direction;
1334    const screenshots = this.shadowRoot.querySelectorAll(".screenshot");
1335
1336    if (this.currentIndex < 0) {
1337      this.currentIndex = screenshots.length - 1;
1338    } else if (this.currentIndex >= screenshots.length) {
1339      this.currentIndex = 0;
1340    }
1341
1342    this.updateCarousel();
1343  }
1344
1345  updateCarousel() {
1346    const screenshots = this.shadowRoot.querySelectorAll(".screenshot");
1347    const dots = this.shadowRoot.querySelectorAll(".dot");
1348    const total = screenshots.length;
1349
1350    screenshots.forEach((screenshot, index) => {
1351      const position = index - this.currentIndex;
1352      const absPosition = Math.abs(position);
1353
1354      let translateX = position * 150;
1355      let translateZ = -absPosition * 180;
1356      let rotateY = position * -35;
1357      let scale = 1 - absPosition * 0.15;
1358      let opacity = absPosition === 0 ? 1 : 0.6 - absPosition * 0.1;
1359      let zIndex = total - absPosition;
1360
1361      if (absPosition > 2) {
1362        opacity = 0;
1363        scale = 0.7;
1364      }
1365
1366      screenshot.style.transform = `
1367        translateX(${translateX}px) 
1368        translateZ(${translateZ}px) 
1369        rotateY(${rotateY}deg) 
1370        scale(${scale})
1371      `;
1372      screenshot.style.opacity = opacity;
1373      screenshot.style.zIndex = zIndex;
1374    });
1375
1376    // Update dots
1377    dots.forEach((dot, index) => {
1378      dot.classList.toggle("active", index === this.currentIndex);
1379    });
1380  }
1381
1382  open() {
1383    this.setAttribute("open", "");
1384  }
1385
1386  close() {
1387    this.removeAttribute("open");
1388  }
1389
1390  updateVisibility() {
1391    if (this.isDialog && !this.dialogSeen) return;
1392
1393    const backdrop = this.shadowRoot.querySelector(".backdrop");
1394    const sheet = this.shadowRoot.querySelector(".sheet");
1395    const isOpen = this.hasAttribute("open");
1396    const isMini = this.hasAttribute("mini");
1397
1398    if (isOpen || isMini) {
1399      sheet.classList.add("show");
1400
1401      if (isMini) {
1402        sheet.classList.add("mini");
1403        backdrop.classList.add("mini");
1404        backdrop.classList.remove("show");
1405      } else {
1406        sheet.classList.remove("mini");
1407        backdrop.classList.remove("mini");
1408        backdrop.classList.add("show");
1409      }
1410    } else {
1411      backdrop.classList.remove("show");
1412      backdrop.classList.remove("mini");
1413      sheet.classList.remove("show");
1414      sheet.classList.remove("mini");
1415    }
1416  }
1417
1418  renderDialog() {
1419    if (this._dialogTimer) {
1420      clearInterval(this._dialogTimer);
1421      this._dialogTimer = null;
1422    }
1423
1424    const isGerman = this.language === "de";
1425    const isIOS = !this.isAndroid;
1426    const storeUrl = this.getStoreUrl();
1427
1428    const title = isGerman ? "Zitate als Video exportieren" : "Quotes as Video";
1429
1430    const buttonText = isGerman ? (isIOS ? "Jetzt gratis im App Store" : "Jetzt gratis bei Google Play") : "Download Free";
1431
1432    this.shadowRoot.innerHTML = `
1433    <style>
1434      * {
1435        box-sizing: border-box;
1436      }
1437
1438      .backdrop {
1439        position: fixed;
1440        inset: 0;
1441        background: rgba(0,0,0,.75);
1442        backdrop-filter: blur(6px);
1443        display: flex;
1444        justify-content: center;
1445        align-items: center;
1446        padding: 20px;
1447        z-index: 999999;
1448      }
1449
1450      .dialog {
1451        position: relative;
1452        width: min(560px, 100%);
1453        background: #000;
1454        border-radius: 28px;
1455        overflow: hidden;
1456        box-shadow: 0 30px 80px rgba(0,0,0,.5);
1457      }
1458
1459      .close-button {
1460        position: absolute;
1461        top: 12px;
1462        right: 12px;
1463
1464        width: 42px;
1465        height: 42px;
1466
1467        border: none;
1468        border-radius: 50%;
1469
1470        background: rgba(255,255,255,.15);
1471        color: white;
1472
1473        font-size: 18px;
1474        font-weight: 700;
1475
1476        z-index: 10;
1477        cursor: pointer;
1478
1479        display: flex;
1480        align-items: center;
1481        justify-content: center;
1482      }
1483
1484      .close-button:disabled {
1485        cursor: default;
1486        opacity: .9;
1487      }
1488
1489      .content {
1490        padding: 24px;
1491      }
1492
1493      .title {
1494        color: white;
1495        text-align: center;
1496        font-size: 28px;
1497        font-weight: 800;
1498        line-height: 1.1;
1499        margin-top: 10px;
1500      }
1501
1502      .hero-image {
1503        width: 100%;
1504        display: block;
1505        border-radius: 18px;
1506        cursor: pointer;
1507      }
1508
1509      .download-button {
1510        display: flex;
1511        align-items: center;
1512        justify-content: center;
1513
1514        width: 100%;
1515        min-height: 58px;
1516
1517        margin-top: 20px;
1518
1519        border-radius: 16px;
1520
1521        background: linear-gradient(
1522          90deg,
1523          #ff9500,
1524          #ff3b30
1525        );
1526
1527        color: white;
1528        text-decoration: none;
1529
1530        font-size: 18px;
1531        font-weight: 800;
1532
1533        transition: transform .15s ease;
1534      }
1535
1536      .download-button:hover {
1537        transform: translateY(-2px);
1538      }
1539
1540      @media (max-width: 600px) {
1541        .content {
1542          padding: 16px;
1543        }
1544
1545        .title {
1546          font-size: 22px;
1547        }
1548
1549        .subtitle {
1550          font-size: 15px;
1551        }
1552
1553        .download-button {
1554          font-size: 16px;
1555        }
1556      }
1557    </style>
1558
1559    <div class="backdrop">
1560      <div class="dialog">
1561
1562        <button
1563          class="close-button"
1564          disabled
1565          aria-label="Close"
1566        >
1567          <span class="countdown">5</span>
1568        </button>
1569
1570        <div class="content">
1571          <img
1572            class="hero-image"
1573            src="https://www.zitatdestages.net/app/sheet/export-video.png"
1574            alt="Video Export"
1575          >
1576
1577          <a
1578            href="${storeUrl}"
1579            target="_blank"
1580            rel="noopener"
1581            class="download-button"
1582          >
1583            ${buttonText}
1584          </a>
1585
1586        </div>
1587      </div>
1588    </div>
1589  `;
1590
1591    const closeButton = this.shadowRoot.querySelector(".close-button");
1592    const countdown = this.shadowRoot.querySelector(".countdown");
1593    const backdrop = this.shadowRoot.querySelector(".backdrop");
1594    const heroImage = this.shadowRoot.querySelector(".hero-image");
1595
1596    const closeDialog = () => {
1597      clearInterval(this._dialogTimer);
1598      this._dialogTimer = null;
1599      document.cookie = "promo_video_export_v1=1; path=/; max-age=86400; SameSite=Lax";
1600      this.render();
1601      this.loadScreenshots();
1602      this.setupEventListeners();
1603      this.setAttribute("mini", "");
1604      this.setAttribute("open", "");
1605      this.updateVisibility();
1606    };
1607
1608    heroImage?.addEventListener("click", () => {
1609      window.open(storeUrl, "_blank", "noopener,noreferrer");
1610    });
1611
1612    let seconds = 5;
1613    this._dialogTimer = setInterval(() => {
1614      seconds--;
1615      if (seconds > 0) {
1616        countdown.textContent = seconds;
1617        return;
1618      }
1619      clearInterval(this._dialogTimer);
1620      this._dialogTimer = null;
1621      closeButton.disabled = false;
1622      countdown.textContent = "✕";
1623    }, 1000);
1624
1625    closeButton.addEventListener("click", () => {
1626      if (closeButton.disabled) return;
1627      closeDialog();
1628    });
1629
1630    backdrop.addEventListener("click", (e) => {
1631      if (e.target === backdrop && !closeButton.disabled) {
1632        closeDialog();
1633      }
1634    });
1635  }
1636
1637  disconnectedCallback() {
1638    if (this._dialogTimer) {
1639      clearInterval(this._dialogTimer);
1640      this._dialogTimer = null;
1641    }
1642  }
1643}
1644
1645customElements.define("app-sheet", AppSheet);

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.