1<!doctype html> 2<html lang="en" data-theme="light"> 3 4<head> 5 <meta charset="utf-8" /> 6 <meta name="viewport" content="width=device-width, initial-scale=1" /> 7 <title>tapedeck.local, local YouTube audio capture</title> 8 <meta name="description" 9 content="A local-first web app that pulls audio from YouTube as MP3, M4A, Opus, WAV, or FLAC. Runs entirely on your machine. Nothing leaves your computer." /> 10 11 <link rel="icon" type="image/svg+xml" href="static/favicon.svg" /> 12 13 <link rel="preconnect" href="https://fonts.googleapis.com" /> 14 <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin /> 15 <link 16 href="https://fonts.googleapis.com/css2?family=Geist+Mono:wght@400;500&family=Geist:wght@400;500;600;700&display=swap" 17 rel="stylesheet" /> 18 19 <style> 20 /* ===== tokens ===== */ 21 :root { 22 --accent: oklch(0.70 0.16 55); 23 --accent-ink: oklch(0.20 0.04 60); 24 --accent-2: oklch(0.78 0.13 60); 25 26 --bg: oklch(0.985 0.004 80); 27 --bg-2: oklch(0.965 0.006 80); 28 --bg-3: oklch(0.945 0.008 80); 29 --surface: oklch(0.99 0.003 80); 30 --line: oklch(0.90 0.006 80); 31 --line-2: oklch(0.94 0.005 80); 32 --ink: oklch(0.20 0.01 60); 33 --ink-2: oklch(0.40 0.01 60); 34 --ink-3: oklch(0.58 0.008 60); 35 --ink-4: oklch(0.72 0.006 60); 36 37 --radius: 10px; 38 --radius-sm: 6px; 39 --radius-lg: 16px; 40 --max: 1120px; 41 } 42 43 :root[data-theme="dark"] { 44 --bg: oklch(0.16 0.006 80); 45 --bg-2: oklch(0.19 0.006 80); 46 --bg-3: oklch(0.22 0.006 80); 47 --surface: oklch(0.21 0.006 80); 48 --line: oklch(0.30 0.008 80); 49 --line-2: oklch(0.26 0.008 80); 50 --ink: oklch(0.96 0.005 80); 51 --ink-2: oklch(0.78 0.005 80); 52 --ink-3: oklch(0.62 0.006 80); 53 --ink-4: oklch(0.48 0.006 80); 54 } 55 56 * { 57 box-sizing: border-box; 58 } 59 60 html { 61 scroll-behavior: smooth; 62 } 63 64 body { 65 margin: 0; 66 background: var(--bg); 67 color: var(--ink); 68 font-family: "Geist", ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif; 69 font-feature-settings: "ss01", "cv11"; 70 -webkit-font-smoothing: antialiased; 71 font-size: 15.5px; 72 line-height: 1.55; 73 transition: background-color .25s ease, color .25s ease; 74 } 75 76 .mono { 77 font-family: "Geist Mono", ui-monospace, "JetBrains Mono", "SFMono-Regular", Menlo, monospace; 78 font-variant-ligatures: none; 79 } 80 81 a { 82 color: inherit; 83 } 84 85 ::selection { 86 background: color-mix(in oklab, var(--accent) 40%, transparent); 87 color: var(--ink); 88 } 89 90 /* grain texture (subtle) */ 91 body::after { 92 content: ""; 93 position: fixed; 94 inset: 0; 95 z-index: 0; 96 pointer-events: none; 97 opacity: 0.045; 98 mix-blend-mode: overlay; 99 background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='240' height='240'><filter id='n'><feTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='2' stitchTiles='stitch'/></filter><rect width='100%25' height='100%25' filter='url(%23n)'/></svg>"); 100 } 101 102 :root[data-theme="dark"] body::after { 103 opacity: 0.06; 104 } 105 106 /* glow blobs */ 107 .glow { 108 position: fixed; 109 pointer-events: none; 110 z-index: 0; 111 width: 720px; 112 height: 720px; 113 border-radius: 50%; 114 filter: blur(120px); 115 opacity: 0.32; 116 } 117 118 .glow-1 { 119 top: -260px; 120 left: -160px; 121 background: radial-gradient(circle, color-mix(in oklab, var(--accent) 65%, transparent), transparent 70%); 122 } 123 124 .glow-2 { 125 top: 540px; 126 right: -260px; 127 background: radial-gradient(circle, color-mix(in oklab, var(--accent-2) 55%, transparent), transparent 70%); 128 opacity: 0.22; 129 } 130 131 :root[data-theme="dark"] .glow-1 { 132 opacity: 0.22; 133 } 134 135 :root[data-theme="dark"] .glow-2 { 136 opacity: 0.16; 137 } 138 139 /* ===== topbar ===== */ 140 .topbar { 141 position: sticky; 142 top: 0; 143 z-index: 50; 144 display: flex; 145 align-items: center; 146 justify-content: space-between; 147 padding: 12px 28px; 148 border-bottom: 1px solid var(--line-2); 149 background: color-mix(in oklab, var(--bg) 80%, transparent); 150 backdrop-filter: saturate(140%) blur(10px); 151 -webkit-backdrop-filter: saturate(140%) blur(10px); 152 } 153 154 .brand { 155 display: flex; 156 align-items: center; 157 gap: 12px; 158 } 159 160 .brand-mark { 161 width: 30px; 162 height: 30px; 163 border-radius: var(--radius-sm); 164 background: var(--ink); 165 color: var(--bg); 166 display: grid; 167 place-items: center; 168 } 169 170 :root[data-theme="dark"] .brand-mark { 171 background: var(--accent); 172 color: var(--accent-ink); 173 } 174 175 .brand-name { 176 font-weight: 600; 177 letter-spacing: -0.01em; 178 font-size: 15px; 179 } 180 181 .brand-dot { 182 color: var(--accent); 183 } 184 185 .brand-sub { 186 font-size: 11px; 187 color: var(--ink-3);
188 margin-top: 1px; 189 } 190 191 .topbar-right { 192 display: flex; 193 gap: 8px; 194 align-items: center; 195 } 196 197 .iconbtn { 198 display: inline-grid; 199 place-items: center; 200 width: 34px; 201 height: 34px; 202 border: 1px solid var(--line); 203 border-radius: 999px; 204 background: var(--surface); 205 color: var(--ink-2); 206 cursor: pointer; 207 transition: border-color .12s, color .12s, background .12s; 208 } 209 210 .iconbtn:hover { 211 border-color: var(--ink-3); 212 color: var(--ink); 213 } 214 215 .btn { 216 display: inline-flex; 217 align-items: center; 218 gap: 8px; 219 padding: 9px 16px; 220 border-radius: 999px; 221 font-size: 13px; 222 text-decoration: none; 223 cursor: pointer; 224 border: 1px solid var(--line); 225 background: var(--surface); 226 color: var(--ink-2); 227 transition: border-color .12s, color .12s, background .12s, transform .08s; 228 white-space: nowrap; 229 } 230 231 .btn:hover { 232 border-color: var(--ink-3); 233 color: var(--ink); 234 } 235 236 .btn:active { 237 transform: translateY(1px); 238 } 239 240 .btn-primary { 241 background: var(--accent); 242 color: var(--accent-ink); 243 border-color: transparent; 244 font-weight: 500; 245 } 246 247 .btn-primary:hover { 248 filter: brightness(.96); 249 color: var(--accent-ink); 250 } 251 252 .btn-coffee { 253 color: oklch(0.45 0.08 50); 254 border-color: color-mix(in oklab, oklch(0.55 0.10 50) 45%, var(--line)); 255 font-weight: 500; 256 } 257 258 .btn-coffee:hover { 259 color: oklch(0.34 0.09 45); 260 border-color: oklch(0.55 0.10 50); 261 background: color-mix(in oklab, oklch(0.55 0.10 50) 6%, var(--surface)); 262 } 263 264 :root[data-theme="dark"] .btn-coffee { 265 color: oklch(0.80 0.08 55); 266 border-color: color-mix(in oklab, oklch(0.70 0.10 55) 45%, var(--line)); 267 } 268 269 :root[data-theme="dark"] .btn-coffee:hover { 270 color: oklch(0.90 0.08 55); 271 border-color: oklch(0.70 0.10 55); 272 background: color-mix(in oklab, oklch(0.70 0.10 55) 10%, var(--surface)); 273 } 274 275 .btn-ghost { 276 background: transparent; 277 } 278 279 .btn svg { 280 display: block; 281 } 282 283 /* ===== sections ===== */ 284 section { 285 position: relative; 286 z-index: 1; 287 } 288 289 .wrap { 290 max-width: var(--max); 291 margin: 0 auto; 292 padding: 0 28px; 293 } 294 295 /* ===== hero ===== */ 296 .hero { 297 padding: 88px 0 60px; 298 text-align: center; 299 } 300 301 .eyebrow { 302 display: inline-flex; 303 align-items: center; 304 gap: 8px; 305 padding: 5px 12px; 306 border-radius: 999px; 307 background: color-mix(in oklab, var(--accent) 12%, var(--surface)); 308 border: 1px solid color-mix(in oklab, var(--accent) 30%, var(--line)); 309 color: var(--ink); 310 font-size: 12px; 311 } 312 313 .eyebrow .dot { 314 width: 6px; 315 height: 6px; 316 border-radius: 50%; 317 background: var(--accent); 318 box-shadow: 0 0 0 4px color-mix(in oklab, var(--accent) 18%, transparent); 319 } 320 321 .hero h1 { 322 margin: 22px auto 16px; 323 font-size: clamp(40px, 6.4vw, 76px); 324 line-height: 1.02; 325 letter-spacing: -0.035em; 326 font-weight: 600; 327 max-width: 18ch; 328 } 329 330 .hero h1 em { 331 font-style: normal; 332 background: linear-gradient(180deg, var(--accent) 0%, color-mix(in oklab, var(--accent) 60%, var(--ink)) 100%); 333 -webkit-background-clip: text; 334 background-clip: text; 335 color: transparent; 336 } 337 338 .hero p.lede { 339 margin: 0 auto; 340 max-width: 60ch; 341 color: var(--ink-2); 342 font-size: clamp(16px, 1.6vw, 18px); 343 } 344 345 .cta-row { 346 display: flex; 347 flex-wrap: wrap; 348 gap: 12px; 349 justify-content: center; 350 margin-top: 32px; 351 } 352 353 .btn-lg { 354 padding: 12px 22px; 355 font-size: 14px; 356 border-radius: 999px; 357 } 358 359 .hero-meta { 360 display: flex; 361 flex-wrap: wrap; 362 gap: 6px 18px; 363 justify-content: center; 364 margin-top: 26px; 365 color: var(--ink-3); 366 font-size: 12.5px; 367 } 368 369 .hero-meta span { 370 display: inline-flex; 371 align-items: center; 372 gap: 8px; 373 } 374 375 .hero-meta .sep { 376 color: var(--ink-4); 377 } 378 379 .hero-meta .pill { 380 padding: 3px 9px; 381 border-radius: 999px; 382 border: 1px solid var(--line-2); 383 background: var(--surface); 384 font-size: 11px; 385 } 386 387 /* ===== screenshot frame ===== */ 388 .shot { 389 position: relative; 390 border-radius: var(--radius-lg); 391 background: var(--surface); 392 border: 1px solid var(--line); 393 box-shadow: 394 0 1px 0 color-mix(in oklab, var(--ink) 4%, transparent) inset, 395 0 30px 80px -30px color-mix(in oklab, var(--ink) 18%, transparent), 396 0 8px 24px -10px color-mix(in oklab, var(--ink) 8%, transparent); 397 overflow: hidden; 398 } 399 400 .shot-bar { 401 display: flex; 402 align-items: center; 403 gap: 6px; 404 padding: 11px 14px; 405 border-bottom: 1px solid var(--line-2); 406 background: var(--bg-2); 407 } 408 409 .shot-bar .tl-dot { 410 width: 10px; 411 height: 10px; 412 border-radius: 50%; 413 background: color-mix(in oklab, var(--ink) 18%, var(--bg-3)); 414 } 415 416 .shot-bar .tl-dot:nth-child(1) { 417 background: oklch(0.72 0.16 25); 418 } 419 420 .shot-bar .tl-dot:nth-child(2) { 421 background: oklch(0.80 0.14 80); 422 } 423 424 .shot-bar .tl-dot:nth-child(3) { 425 background: oklch(0.70 0.12 155); 426 } 427 428 .shot-bar .tl-addr { 429 margin-left: 10px; 430 font-family: "Geist Mono", ui-monospace, monospace; 431 font-size: 11.5px; 432 color: var(--ink-3); 433 padding: 3px 10px; 434 border-radius: 999px; 435 border: 1px solid var(--line-2); 436 background: var(--surface); 437 } 438 439 .shot-body { 440 position: relative; 441 aspect-ratio: 16 / 10; 442 background: 443 radial-gradient(800px 400px at 50% -20%, color-mix(in oklab, var(--accent) 8%, transparent), transparent 60%), 444 repeating-linear-gradient(0deg, color-mix(in oklab, var(--ink) 4%, transparent) 0 1px, transparent 1px 32px), 445 repeating-linear-gradient(90deg, color-mix(in oklab, var(--ink) 4%, transparent) 0 1px, transparent 1px 32px), 446 var(--bg); 447 display: grid; 448 place-items: center;
449 color: var(--ink-3); 450 } 451 452 .shot-body::before { 453 content: ""; 454 position: absolute; 455 inset: 0; 456 background: radial-gradient(circle at 50% 50%, transparent 30%, color-mix(in oklab, var(--bg) 50%, transparent) 100%); 457 pointer-events: none; 458 } 459 460 .shot-tag { 461 position: absolute; 462 top: 12px; 463 left: 12px; 464 z-index: 2; 465 font-family: "Geist Mono", ui-monospace, monospace; 466 font-size: 11px; 467 color: var(--ink); 468 padding: 4px 10px; 469 border-radius: 999px; 470 background: color-mix(in oklab, var(--surface) 88%, transparent); 471 border: 1px solid var(--line); 472 backdrop-filter: blur(4px); 473 } 474 475 .shot-tag::before { 476 content: "ð¸ "; 477 opacity: .65; 478 } 479 480 .shot-placeholder { 481 position: relative; 482 z-index: 1; 483 display: flex; 484 flex-direction: column; 485 align-items: center; 486 gap: 14px; 487 max-width: 80%; 488 text-align: center; 489 } 490 491 .shot-placeholder svg { 492 opacity: .35; 493 } 494 495 .shot-placeholder-title { 496 font-size: 14px; 497 font-weight: 500; 498 color: var(--ink-2); 499 letter-spacing: -0.01em; 500 } 501 502 .shot-placeholder-sub { 503 font-size: 12.5px; 504 color: var(--ink-3); 505 max-width: 50ch; 506 } 507 508 .shot-body.has-image { 509 aspect-ratio: auto; 510 background: var(--bg-2); 511 display: block; 512 padding: 0; 513 } 514 515 .shot-body.has-image::before { 516 display: none; 517 } 518 519 .shot-body.has-image>img { 520 display: block; 521 width: 100%; 522 height: auto; 523 } 524 525 .shot-caption { 526 margin-top: 14px; 527 display: flex; 528 flex-wrap: wrap; 529 gap: 8px 14px; 530 align-items: baseline; 531 } 532 533 .shot-caption .file { 534 font-family: "Geist Mono", ui-monospace, monospace; 535 font-size: 11.5px; 536 color: var(--ink); 537 padding: 3px 10px; 538 border-radius: 5px; 539 background: color-mix(in oklab, var(--accent) 11%, var(--surface)); 540 border: 1px solid color-mix(in oklab, var(--accent) 28%, var(--line)); 541 white-space: nowrap; 542 } 543 544 .shot-caption .desc { 545 color: var(--ink-3); 546 font-size: 13.5px; 547 line-height: 1.5; 548 } 549 550 .hero-shot-wrap { 551 max-width: 980px; 552 margin: 56px auto 0; 553 padding: 0 28px; 554 } 555 556 /* ===== section header ===== */ 557 .sec-head { 558 display: flex; 559 flex-direction: column; 560 gap: 10px; 561 margin-bottom: 36px; 562 } 563 564 .sec-eyebrow { 565 font-family: "Geist Mono", ui-monospace, monospace; 566 font-size: 11px; 567 letter-spacing: 0.18em; 568 text-transform: uppercase; 569 color: var(--accent); 570 } 571 572 .sec-title { 573 margin: 0; 574 font-size: clamp(28px, 3.6vw, 40px); 575 letter-spacing: -0.025em; 576 line-height: 1.05; 577 font-weight: 600; 578 max-width: 22ch; 579 } 580 581 .sec-sub { 582 margin: 0; 583 color: var(--ink-2); 584 max-width: 60ch; 585 font-size: 15.5px; 586 } 587 588 /* ===== feature grid ===== */ 589 .features { 590 display: grid; 591 gap: 16px; 592 grid-template-columns: repeat(4, minmax(0, 1fr)); 593 } 594 595 .feature { 596 display: flex; 597 flex-direction: column; 598 gap: 10px; 599 padding: 22px; 600 background: var(--surface); 601 border: 1px solid var(--line-2); 602 border-radius: var(--radius); 603 transition: border-color .15s, transform .15s; 604 } 605 606 .feature:hover { 607 border-color: color-mix(in oklab, var(--accent) 35%, var(--line)); 608 transform: translateY(-2px); 609 } 610 611 .feature-icon { 612 width: 36px; 613 height: 36px; 614 border-radius: var(--radius-sm); 615 background: color-mix(in oklab, var(--accent) 14%, var(--surface)); 616 color: var(--accent); 617 display: grid; 618 place-items: center; 619 border: 1px solid color-mix(in oklab, var(--accent) 25%, var(--line)); 620 } 621 622 .feature h3 { 623 margin: 4px 0 0; 624 font-size: 15.5px; 625 font-weight: 600; 626 letter-spacing: -0.01em; 627 } 628 629 .feature p { 630 margin: 0; 631 color: var(--ink-2); 632 font-size: 13.5px; 633 line-height: 1.55; 634 } 635 636 @media (max-width: 1024px) { 637 .features { 638 grid-template-columns: repeat(2, 1fr); 639 } 640 } 641 642 @media (max-width: 560px) { 643 .features { 644 grid-template-columns: 1fr; 645 } 646 } 647 648 /* ===== alternating screenshot blocks ===== */ 649 .split { 650 display: grid; 651 gap: 48px; 652 grid-template-columns: 0.9fr 1.1fr; 653 align-items: center; 654 } 655 656 .split.reverse { 657 grid-template-columns: 1.1fr 0.9fr; 658 } 659 660 .split.reverse .split-text { 661 order: 2; 662 } 663 664 .split-text h2 { 665 margin: 0 0 12px; 666 font-size: clamp(24px, 2.8vw, 32px); 667 letter-spacing: -0.02em; 668 font-weight: 600; 669 } 670 671 .split-text p { 672 margin: 0; 673 color: var(--ink-2); 674 font-size: 15px; 675 line-height: 1.6; 676 } 677 678 .split-text ul { 679 list-style: none; 680 padding: 0; 681 margin: 18px 0 0; 682 display: flex; 683 flex-direction: column; 684 gap: 8px; 685 } 686 687 .split-text li { 688 display: flex; 689 gap: 10px; 690 align-items: flex-start; 691 font-size: 13.5px; 692 color: var(--ink-2); 693 } 694 695 .split-text li::before { 696 content: ""; 697 flex: 0 0 auto; 698 width: 6px; 699 height: 6px; 700 border-radius: 50%; 701 background: var(--accent); 702 margin-top: 8px; 703 } 704 705 @media (max-width: 880px) { 706 707 .split, 708 .split.reverse { 709 grid-template-columns: 1fr; 710 gap: 24px; 711 } 712 713 .split.reverse .split-text { 714 order: 0; 715 } 716 } 717 718 /* ===== formats strip ===== */ 719 .formats { 720 display: flex; 721 flex-wrap: wrap; 722 gap: 10px; 723 justify-content: center; 724 margin-top: 22px; 725 } 726 727 .format-chip { 728 display: inline-flex; 729 align-items: center; 730 gap: 8px; 731 padding: 7px 13px; 732 border-radius: 999px; 733 background: var(--surface); 734 border: 1px solid var(--line); 735 font-family: "Geist Mono", ui-monospace, monospace; 736 font-size: 12px; 737 color: var(--ink-2); 738 } 739 740 .format-chip .kbps { 741 color: var(--ink-4); 742 font-size: 11px; 743 padding-left: 8px; 744 margin-left: 4px; 745 border-left: 1px solid var(--line-2); 746 } 747 748 /* ===== how it works ===== */ 749 .stack { 750 display: grid; 751 gap: 12px; 752 grid-template-columns: repeat(4, minmax(0, 1fr)); 753 } 754 755 .stack-card { 756 display: flex; 757 flex-direction: column; 758 gap: 8px; 759 padding: 18px; 760 background: var(--surface); 761 border: 1px solid var(--line-2); 762 border-radius: var(--radius); 763 } 764 765 .stack-card .role { 766 font-family: "Geist Mono", ui-monospace, monospace; 767 font-size: 10.5px; 768 letter-spacing: 0.15em; 769 text-transform: uppercase;
770 color: var(--ink-3); 771 } 772 773 .stack-card .name { 774 font-size: 16px; 775 font-weight: 600; 776 letter-spacing: -0.01em; 777 } 778 779 .stack-card .desc { 780 color: var(--ink-3); 781 font-size: 12.5px; 782 line-height: 1.55; 783 } 784 785 @media (max-width: 880px) { 786 .stack { 787 grid-template-columns: 1fr 1fr; 788 } 789 } 790 791 /* ===== CTA card ===== */ 792 .cta-card { 793 position: relative; 794 margin: 80px 0 60px; 795 padding: 48px 36px; 796 border-radius: var(--radius-lg); 797 background: 798 radial-gradient(800px 280px at 100% 0%, color-mix(in oklab, var(--accent) 22%, transparent), transparent 60%), 799 radial-gradient(600px 240px at 0% 100%, color-mix(in oklab, var(--accent-2) 20%, transparent), transparent 60%), 800 var(--surface); 801 border: 1px solid var(--line); 802 overflow: hidden; 803 text-align: center; 804 } 805 806 .cta-card::before { 807 content: ""; 808 position: absolute; 809 inset: 0; 810 background: 811 repeating-linear-gradient(0deg, color-mix(in oklab, var(--ink) 3%, transparent) 0 1px, transparent 1px 24px), 812 repeating-linear-gradient(90deg, color-mix(in oklab, var(--ink) 3%, transparent) 0 1px, transparent 1px 24px); 813 pointer-events: none; 814 mask-image: radial-gradient(circle at 50% 50%, black 30%, transparent 75%); 815 } 816 817 .cta-card>* { 818 position: relative; 819 z-index: 1; 820 } 821 822 .cta-card h2 { 823 margin: 0 0 12px; 824 font-size: clamp(28px, 3.4vw, 38px); 825 letter-spacing: -0.025em; 826 font-weight: 600; 827 } 828 829 .cta-card p { 830 margin: 0 auto 26px; 831 max-width: 56ch; 832 color: var(--ink-2); 833 font-size: 15.5px; 834 } 835 836 /* ===== footer ===== */ 837 .footer { 838 position: relative; 839 border-top: 1px solid var(--line-2); 840 margin-top: 0; 841 background: var(--bg); 842 } 843 844 .footer-accent { 845 position: absolute; 846 top: -1px; 847 left: 0; 848 right: 0; 849 height: 2px; 850 background: linear-gradient(90deg, 851 transparent 0%, 852 color-mix(in oklab, var(--accent) 60%, transparent) 18%, 853 color-mix(in oklab, var(--accent) 60%, transparent) 82%, 854 transparent 100%); 855 opacity: 0.55; 856 pointer-events: none; 857 } 858 859 .footer-inner { 860 max-width: var(--max); 861 margin: 0 auto; 862 padding: 22px 28px; 863 display: flex; 864 align-items: center; 865 justify-content: space-between; 866 gap: 28px; 867 flex-wrap: wrap; 868 } 869 870 .footer-left { 871 display: flex; 872 align-items: center; 873 gap: 12px; 874 } 875 876 .footer-mark { 877 width: 28px; 878 height: 28px; 879 border-radius: var(--radius-sm); 880 background: var(--ink); 881 color: var(--bg); 882 display: inline-grid; 883 place-items: center; 884 } 885 886 :root[data-theme="dark"] .footer-mark { 887 background: var(--accent); 888 color: var(--accent-ink); 889 } 890 891 .footer-brand { 892 font-weight: 600; 893 font-size: 14px; 894 letter-spacing: -0.01em; 895 } 896 897 .footer-version { 898 font-size: 11px; 899 color: var(--ink-3); 900 padding: 2px 9px; 901 border: 1px solid var(--line); 902 border-radius: 999px; 903 background: var(--surface); 904 } 905 906 .footer-right { 907 display: flex; 908 align-items: center; 909 gap: 24px; 910 flex-wrap: wrap; 911 } 912 913 .footer-link { 914 color: var(--ink-3); 915 text-decoration: none; 916 display: inline-flex; 917 align-items: center; 918 gap: 7px; 919 font-size: 13px; 920 padding: 2px 0; 921 border-bottom: 1px dotted transparent; 922 transition: color .12s, border-color .12s; 923 } 924 925 .footer-link:hover { 926 color: var(--ink); 927 border-bottom-color: var(--ink-3); 928 } 929 930 .footer-arrow { 931 font-size: 12px; 932 opacity: 0.65; 933 margin-left: 2px; 934 } 935 936 /* ===== reel motif (decorative section divider) ===== */ 937 .divider { 938 display: flex; 939 align-items: center; 940 gap: 14px; 941 margin: 60px auto; 942 max-width: 360px; 943 color: var(--ink-4); 944 } 945 946 .divider .line { 947 flex: 1; 948 height: 1px; 949 background: var(--line); 950 } 951 952 .reel-mini { 953 width: 22px; 954 height: 22px; 955 animation: reel-spin 4s linear infinite; 956 transform-box: fill-box; 957 transform-origin: center; 958 } 959 960 @keyframes reel-spin { 961 to { 962 transform: rotate(360deg); 963 } 964 } 965 966 @media (prefers-reduced-motion: reduce) { 967 .reel-mini { 968 animation: none; 969 } 970 } 971 972 .section-pad { 973 padding: 64px 0; 974 } 975 976 .section-pad-sm { 977 padding: 44px 0; 978 } 979 980 /* fade-in on scroll */ 981 .reveal { 982 opacity: 0; 983 transform: translateY(14px); 984 transition: opacity .7s ease, transform .7s ease; 985 } 986 987 .reveal.in { 988 opacity: 1; 989 transform: none; 990 } 991 992 @media (prefers-reduced-motion: reduce) { 993 .reveal { 994 opacity: 1; 995 transform: none; 996 transition: none; 997 } 998 } 999 1000 @media (max-width: 560px) { 1001 .topbar { 1002 padding: 10px 16px; 1003 } 1004 1005 .wrap, 1006 .hero-shot-wrap { 1007 padding: 0 16px; 1008 } 1009
1010 .topbar-right .btn span:not(.kbd) { 1011 display: none; 1012 } 1013 1014 .topbar-right .btn { 1015 padding: 9px 11px; 1016 } 1017 1018 .cta-card { 1019 padding: 36px 20px; 1020 } 1021 } 1022 </style> 1023</head> 1024 1025<body> 1026 1027 <div class="glow glow-1" aria-hidden></div> 1028 <div class="glow glow-2" aria-hidden></div> 1029 1030 <!-- ===== topbar ===== --> 1031 <header class="topbar"> 1032 <div class="brand"> 1033 <div class="brand-mark" aria-hidden> 1034 <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" 1035 width="16" height="16"> 1036 <path d="M3 12h2M7 8v8M11 5v14M15 9v6M19 11v2M21 12h.01" /> 1037 </svg> 1038 </div> 1039 <div> 1040 <div class="brand-name">tapedeck<span class="brand-dot">.</span>local</div> 1041 <div class="brand-sub mono">v1.1 · runs on your machine</div> 1042 </div> 1043 </div> 1044 <div class="topbar-right"> 1045 <button id="themeToggle" class="iconbtn" aria-label="toggle theme" title="toggle theme"> 1046 <svg id="themeIcon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" 1047 stroke-linecap="round" stroke-linejoin="round" width="15" height="15"> 1048 <path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z" /> 1049 </svg> 1050 </button> 1051 <a class="btn btn-primary" href="https://github.com/cristiangabriel88/localYoutubeToMp3#readme" target="_blank" 1052 rel="noopener"> 1053 <svg viewBox="0 0 24 24" fill="currentColor" width="15" height="15" aria-hidden> 1054 <path 1055 d="M12 .5C5.65.5.5 5.65.5 12c0 5.08 3.29 9.39 7.86 10.91.57.11.78-.25.78-.55v-1.93c-3.2.7-3.87-1.54-3.87-1.54-.53-1.34-1.29-1.7-1.29-1.7-1.05-.72.08-.7.08-.7 1.17.08 1.78 1.2 1.78 1.2 1.04 1.78 2.72 1.27 3.38.97.11-.75.41-1.27.74-1.56-2.55-.29-5.24-1.28-5.24-5.71 0-1.26.45-2.29 1.19-3.1-.12-.29-.52-1.47.11-3.07 0 0 .98-.31 3.2 1.18a11.1 11.1 0 0 1 5.83 0c2.22-1.49 3.2-1.18 3.2-1.18.63 1.6.23 2.78.11 3.07.74.81 1.19 1.84 1.19 3.1 0 4.44-2.69 5.41-5.26 5.7.42.36.79 1.07.79 2.16v3.2c0 .31.21.67.79.55C20.21 21.39 23.5 17.08 23.5 12 23.5 5.65 18.35.5 12 .5z" /> 1056 </svg> 1057 <span>Get it on GitHub</span> 1058 </a> 1059 <a class="btn btn-coffee" href="https://ko-fi.com/s/1bd08ce885" target="_blank" rel="noopener"> 1060 <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" 1061 stroke-linejoin="round" width="15" height="15" aria-hidden> 1062 <path d="M4 9h13v6a4 4 0 0 1-4 4H8a4 4 0 0 1-4-4V9z" /> 1063 <path d="M17 11h2a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-2" /> 1064 <path d="M7 3v3M10 3v3M13 3v3" /> 1065 </svg> 1066 <span>Buy me a coffee</span> 1067 </a> 1068 </div> 1069 </header> 1070 1071 <!-- ===== hero ===== --> 1072 <section class="hero"> 1073 <div class="wrap"> 1074 <div class="eyebrow"><span class="dot"></span><span>Local-first audio capture · open source</span></div> 1075 <h1>Web audio, extracted <em>locally and privately.</em></h1> 1076 <p class="lede"> 1077 <span class="mono">tapedeck.local</span> pulls audio from YouTube and 1000+ other sites as MP3, M4A, 1078 Opus, WAV, or FLAC, 1079 entirely on your machine. No cloud. No browser save dialog. Just files where you wanted them. 1080 </p> 1081 <div class="cta-row"> 1082 <a class="btn btn-primary btn-lg" href="https://github.com/cristiangabriel88/localYoutubeToMp3#readme" 1083 target="_blank" rel="noopener"> 1084 <svg viewBox="0 0 24 24" fill="currentColor" width="16" height="16" aria-hidden> 1085 <path 1086 d="M12 .5C5.65.5.5 5.65.5 12c0 5.08 3.29 9.39 7.86 10.91.57.11.78-.25.78-.55v-1.93c-3.2.7-3.87-1.54-3.87-1.54-.53-1.34-1.29-1.7-1.29-1.7-1.05-.72.08-.7.08-.7 1.17.08 1.78 1.2 1.78 1.2 1.04 1.78 2.72 1.27 3.38.97.11-.75.41-1.27.74-1.56-2.55-.29-5.24-1.28-5.24-5.71 0-1.26.45-2.29 1.19-3.1-.12-.29-.52-1.47.11-3.07 0 0 .98-.31 3.2 1.18a11.1 11.1 0 0 1 5.83 0c2.22-1.49 3.2-1.18 3.2-1.18.63 1.6.23 2.78.11 3.07.74.81 1.19 1.84 1.19 3.1 0 4.44-2.69 5.41-5.26 5.7.42.36.79 1.07.79 2.16v3.2c0 .31.21.67.79.55C20.21 21.39 23.5 17.08 23.5 12 23.5 5.65 18.35.5 12 .5z" /> 1087 </svg> 1088 Get it on GitHub 1089 </a> 1090 <a class="btn btn-lg" href="#how">See how it works</a> 1091 </div> 1092 <div class="hero-meta"> 1093 <span class="pill mono">Python 3.8+</span> 1094 <span class="sep">·</span> 1095 <span>FFmpeg-powered</span> 1096 <span class="sep">·</span> 1097 <span>Single-exe build</span> 1098 <span class="sep">·</span> 1099 <span>Light + dark</span> 1100 </div> 1101 </div> 1102 1103 <!-- hero screenshot --> 1104 <div class="hero-shot-wrap reveal"> 1105 <div class="shot"> 1106 <div class="shot-bar"> 1107 <span class="tl-dot"></span><span class="tl-dot"></span><span class="tl-dot"></span> 1108 <span class="tl-addr">http://127.0.0.1:5050 · tapedeck.local</span> 1109 </div> 1110 <div class="shot-body has-image"> 1111 <img src="screenshots/01-main-view.png" 1112 alt="Main view: paste, watch reels spin, file lands on disk." /> 1113 </div> 1114 </div> 1115 </div> 1116 </section> 1117 1118 <div class="wrap"> 1119 <div class="divider" aria-hidden> 1120 <span class="line"></span> 1121 <svg viewBox="-10 -10 20 20" class="reel-mini" fill="none" stroke="currentColor" stroke-width="1.2"> 1122 <circle cx="0" cy="0" r="6.5" /> 1123 <circle cx="0" cy="0" r="1.4" fill="currentColor" /> 1124 <line x1="0" y1="-5" x2="0" y2="5" /> 1125 <line x1="-5" y1="0" x2="5" y2="0" /> 1126 </svg> 1127 <span class="line"></span> 1128 </div> 1129 </div> 1130 1131 <!-- ===== why three ===== --> 1132 <section class="section-pad"> 1133 <div class="wrap"> 1134 <div class="sec-head reveal"> 1135 <span class="sec-eyebrow">Why tapedeck</span> 1136 <h2 class="sec-title">Four opinions web downloaders missed.</h2> 1137 <p class="sec-sub">Built for the small frictions: where files go, what they're named, what the page is 1138 trying to push on you, and whether the web app feels like an app or a script in disguise.</p> 1139 </div> 1140 <div class="features reveal"> 1141 <div class="feature"> 1142 <div class="feature-icon"> 1143 <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" 1144 stroke-linecap="round" stroke-linejoin="round" width="18" height="18"> 1145 <rect x="2" y="6" width="20" height="14" rx="2" /> 1146 <path d="M2 10h20" /> 1147 <circle cx="6" cy="15" r="1.4" fill="currentColor" /> 1148 </svg> 1149 </div> 1150 <h3>Disk-save, not browser-save</h3> 1151 <p>Files land directly in your configured folder, no <span class="mono">~/Downloads</span> detour, 1152 no save dialog, no mystery filename.</p> 1153 </div> 1154 <div class="feature"> 1155 <div class="feature-icon"> 1156 <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" 1157 stroke-linecap="round" stroke-linejoin="round" width="18" height="18"> 1158 <path d="M3 12a9 9 0 0 1 18 0v3a3 3 0 0 1-3 3h-1v-6h4" /> 1159 <path d="M3 12v3a3 3 0 0 0 3 3h1v-6H3" /> 1160 </svg> 1161 </div> 1162 <h3>Local, period.</h3> 1163 <p>Flask + yt-dlp + FFmpeg, all on <span class="mono">127.0.0.1</span>. Nothing leaves your 1164 computer. No accounts. No telemetry. No API.</p> 1165 </div> 1166 <div class="feature"> 1167 <div class="feature-icon"> 1168 <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" 1169 stroke-linecap="round" stroke-linejoin="round" width="18" height="18"> 1170 <path d="M12 2l8 4v6c0 5-3.5 8.5-8 10-4.5-1.5-8-5-8-10V6l8-4z" /> 1171 <path d="M9 12l2 2 4-4" /> 1172 </svg> 1173 </div> 1174 <h3>No ads. No scams. No malware.</h3> 1175 <p>The usual "YouTube to MP3" sites are a minefield of pop-ups, fake download buttons, sketchy
1176 installers, and trackers. This is a static page on <span class="mono">localhost</span>, with 1177 zero 1178 ads, zero redirects, zero third parties watching.</p> 1179 </div> 1180 <div class="feature"> 1181 <div class="feature-icon"> 1182 <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" 1183 stroke-linecap="round" stroke-linejoin="round" width="18" height="18"> 1184 <path d="M3 7a2 2 0 0 1 2-2h4l2 2h8a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V7z" /> 1185 <path d="M9 14l2 2 4-4" /> 1186 </svg> 1187 </div> 1188 <h3>File-manager native</h3> 1189 <p>Reveal-in-folder pops Explorer / Finder / your file manager focused on the exact file. 1190 Collision-safe names. Zero hunt-and-peck.</p> 1191 </div> 1192 </div> 1193 </div> 1194 </section> 1195 1196 <!-- ===== split: queue / phase progress ===== --> 1197 <section class="section-pad"> 1198 <div class="wrap"> 1199 <div class="split"> 1200 <div class="split-text reveal"> 1201 <span class="sec-eyebrow">The queue</span> 1202 <h2>Phase-aware progress, in plain words.</h2> 1203 <p>Each row narrates its own state: <span class="mono">resolving⦠â queued â downloading 47% â 1204 converting⦠â â saved</span>, backed by a slim accent-coloured fill bar tied to real 1205 yt-dlp progress hooks. No spinners pretending to be busy, no localhost transfer theatre.</p> 1206 <ul> 1207 <li>Up to 4 parallel downloads, configurable</li> 1208 <li>Click a queued row to promote it to front of line</li> 1209 <li>Retry failed, clear â, clear all: one click each</li> 1210 <li>Queue survives reloads & re-attaches to live workers</li> 1211 </ul> 1212 </div> 1213 <div class="reveal"> 1214 <div class="shot"> 1215 <div class="shot-bar"> 1216 <span class="tl-dot"></span><span class="tl-dot"></span><span class="tl-dot"></span> 1217 <span class="tl-addr">http://127.0.0.1:5050 · tapedeck.local</span> 1218 </div> 1219 <div class="shot-body has-image"> 1220 <img src="screenshots/02-queue-working.png" alt="Active queue with two rows mid-download" /> 1221 </div> 1222 </div> 1223 </div> 1224 </div> 1225 </div> 1226 </section> 1227 1228 <!-- ===== formats strip ===== --> 1229 <section class="section-pad-sm"> 1230 <div class="wrap"> 1231 <div class="sec-head reveal" style="text-align:center; align-items:center;"> 1232 <span class="sec-eyebrow">Formats</span> 1233 <h2 class="sec-title" style="margin: 0 auto;">Five formats. Four bitrates. Persisted preferences.</h2> 1234 </div> 1235 <div class="formats reveal"> 1236 <span class="format-chip">mp3 <span class="kbps">128 · 192 · 256 · 320</span></span> 1237 <span class="format-chip">m4a <span class="kbps">aac</span></span> 1238 <span class="format-chip">opus <span class="kbps">webm</span></span> 1239 <span class="format-chip">wav <span class="kbps">lossless</span></span> 1240 <span class="format-chip">flac <span class="kbps">lossless</span></span> 1241 </div> 1242 </div> 1243 </section> 1244 1245 <!-- ===== split: filename builder ===== --> 1246 <section class="section-pad"> 1247 <div class="wrap"> 1248 <div class="split reverse"> 1249 <div class="split-text reveal"> 1250 <span class="sec-eyebrow">Filename template</span> 1251 <h2>Drag pills. Name files. Move on.</h2> 1252 <p>The filename builder is a drag-and-drop pill canvas: drop a token (<span 1253 class="mono">{title}</span>, <span class="mono">{channel}</span>, <span 1254 class="mono">{uploader}</span>, <span class="mono">{id}</span>), drop a separator (<span 1255 class="mono">â</span>, <span class="mono">·</span>, <span class="mono">_</span>, space), 1256 reorder by dragging, click à to remove. A live preview shows the rendered filename.</p> 1257 <ul> 1258 <li>Drop anywhere, pills snap into place</li> 1259 <li>Live preview using sample metadata</li> 1260 <li>Persists in <span class="mono">config.json</span> alongside output folder</li> 1261 </ul> 1262 </div> 1263 <div class="reveal"> 1264 <div class="shot"> 1265 <div class="shot-bar"> 1266 <span class="tl-dot"></span><span class="tl-dot"></span><span class="tl-dot"></span> 1267 <span class="tl-addr">http://127.0.0.1:5050 · tapedeck.local</span> 1268 </div> 1269 <div class="shot-body has-image">
1270 <img src="screenshots/03-settings.png" 1271 alt="Settings modal, filename template builder open" /> 1272 </div> 1273 </div> 1274 </div> 1275 </div> 1276 </div> 1277 </section> 1278 1279 <!-- ===== split: dark mode ===== --> 1280 <section class="section-pad"> 1281 <div class="wrap"> 1282 <div class="split"> 1283 <div class="split-text reveal"> 1284 <span class="sec-eyebrow">Themes</span> 1285 <h2>Light by day. Dark by night. Amber either way.</h2> 1286 <p>The whole palette runs on OKLCH tokens, so light and dark match in perceived contrast, and the 1287 amber accent stays itself in both. A grain texture sits on top of everything for the cassette 1288 feel without the cassette weight.</p> 1289 <ul> 1290 <li>Light · dark · accent & density tweaks</li> 1291 <li>Geist + Geist Mono, with ligatures off in mono for clarity</li> 1292 <li>Reduced-motion friendly: reels stop, shimmers pause</li> 1293 </ul> 1294 </div> 1295 <div class="reveal"> 1296 <div class="shot"> 1297 <div class="shot-bar"> 1298 <span class="tl-dot"></span><span class="tl-dot"></span><span class="tl-dot"></span> 1299 <span class="tl-addr">http://127.0.0.1:5050 · tapedeck.local</span> 1300 </div> 1301 <div class="shot-body has-image"> 1302 <img src="screenshots/04-dark-mode.png" 1303 alt="Dark-mode hero: same deck, amber accent shifted to brand mark." /> 1304 </div> 1305 </div> 1306 </div> 1307 </div> 1308 </div> 1309 </section> 1310 1311 <!-- ===== split: multi-paste / clipboard ===== --> 1312 <section class="section-pad"> 1313 <div class="wrap"> 1314 <div class="split reverse"> 1315 <div class="split-text reveal"> 1316 <span class="sec-eyebrow">Input</span> 1317 <h2>Paste anything. We’ll figure it out.</h2> 1318 <p>Drop a browser tab on the page. Paste five URLs separated by spaces, commas, or newlines, and 1319 each 1320 becomes its own row. The app even peeks at your clipboard on focus and offers an "add" pill when 1321 it spots a YouTube link. Press Enter on a duplicate and the existing row shakes so you 1322 don’t doubt you hit the key.</p> 1323 <ul> 1324 <li>Multi-paste, drag-drop, clipboard detect</li> 1325 <li>Playlist expansion: one URL becomes N rows</li> 1326 <li><span class="mono">Ctrl/Cmd+K</span> or <span class="mono">/</span> focuses input from 1327 anywhere</li> 1328 </ul> 1329 </div> 1330 <div class="reveal"> 1331 <div class="shot"> 1332 <div class="shot-bar"> 1333 <span class="tl-dot"></span><span class="tl-dot"></span><span class="tl-dot"></span> 1334 <span class="tl-addr">http://127.0.0.1:5050 · tapedeck.local</span> 1335 </div> 1336 <div class="shot-body has-image"> 1337 <img src="screenshots/05-multi-paste.png" alt="Clipboard hint and expanded playlist" /> 1338 </div> 1339 </div> 1340 </div> 1341 </div> 1342 </div> 1343 </section> 1344 1345 <!-- ===== how it works ===== --> 1346 <section id="how" class="section-pad"> 1347 <div class="wrap"> 1348 <div class="sec-head reveal"> 1349 <span class="sec-eyebrow">How it works</span> 1350 <h2 class="sec-title">Four complex pieces. One nice surface.</h2> 1351 <p class="sec-sub">No build step, no framework lock-in. React 18 via UMD, Babel-standalone for JSX, and 1352 a tiny Flask app gluing yt-dlp and FFmpeg together.</p> 1353 </div> 1354 <div class="stack reveal"> 1355 <div class="stack-card"> 1356 <span class="role">Web server</span> 1357 <span class="name">Flask + Waitress</span> 1358 <span class="desc">Localhost-only WSGI, threaded jobs, polling progress endpoint.</span> 1359 </div> 1360 <div class="stack-card"> 1361 <span class="role">Audio download</span> 1362 <span class="name">yt-dlp</span> 1363 <span class="desc">YouTube + 1000+ sites, progress hooks streamed to the client.</span> 1364 </div> 1365 <div class="stack-card"> 1366 <span class="role">Conversion</span> 1367 <span class="name">FFmpeg</span> 1368 <span class="desc">External binary, auto-detected from PATH or common install dirs.</span> 1369 </div> 1370 <div class="stack-card"> 1371 <span class="role">Frontend</span> 1372 <span class="name">React 18 (UMD)</span> 1373 <span class="desc">JSX via Babel-standalone, no bundler, no node_modules, no build.</span> 1374 </div> 1375 </div> 1376 </div> 1377 </section> 1378 1379 <!-- ===== CTA ===== --> 1380 <section> 1381 <div class="wrap"> 1382 <div class="cta-card reveal"> 1383 <h2>Local audio. Yours to keep.</h2> 1384 <p>Download the zip file from GitHub, uncompress, double-click, done. Your browser opens it 1385 automatically.<br> If 1386 it saves you an afternoon of fighting browser save dialogs and ads, coffee is appreciated.</p> 1387 <div class="cta-row" style="margin-top: 0;"> 1388 <a class="btn btn-primary btn-lg" 1389 href="https://github.com/cristiangabriel88/localYoutubeToMp3#readme" target="_blank" 1390 rel="noopener"> 1391 <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" 1392 stroke-linecap="round" stroke-linejoin="round" width="16" height="16" aria-hidden> 1393 <path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" /> 1394 <polyline points="7 10 12 15 17 10" /> 1395 <line x1="12" y1="15" x2="12" y2="3" /> 1396 </svg> 1397 Get tapedeck 1398 </a> 1399 <a class="btn btn-coffee btn-lg" href="https://ko-fi.com/s/1bd08ce885" target="_blank" 1400 rel="noopener"> 1401 <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" 1402 stroke-linecap="round" stroke-linejoin="round" width="16" height="16" aria-hidden> 1403 <path d="M4 9h13v6a4 4 0 0 1-4 4H8a4 4 0 0 1-4-4V9z" /> 1404 <path d="M17 11h2a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-2" /> 1405 <path d="M7 3v3M10 3v3M13 3v3" /> 1406 </svg> 1407 Buy me a coffee 1408 </a> 1409 </div> 1410 </div> 1411 </div> 1412 </section> 1413 1414 <!-- ===== footer ===== --> 1415 <footer class="footer"> 1416 <div class="footer-accent" aria-hidden></div> 1417 <div class="footer-inner"> 1418 <div class="footer-left"> 1419 <span class="footer-mark" aria-hidden> 1420 <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" 1421 width="14" height="14"> 1422 <path d="M3 12h2M7 8v8M11 5v14M15 9v6M19 11v2M21 12h.01" /> 1423 </svg> 1424 </span> 1425 <span class="footer-brand">tapedeck<span class="brand-dot">.</span>local</span> 1426 <span class="footer-version mono">v1.1</span> 1427 </div> 1428 <div class="footer-right">
1429 <a class="footer-link" href="https://github.com/cristiangabriel88/localYoutubeToMp3" target="_blank" 1430 rel="noopener"> 1431 <svg viewBox="0 0 24 24" fill="currentColor" width="13" height="13" aria-hidden> 1432 <path 1433 d="M12 .5C5.65.5.5 5.65.5 12c0 5.08 3.29 9.39 7.86 10.91.57.11.78-.25.78-.55v-1.93c-3.2.7-3.87-1.54-3.87-1.54-.53-1.34-1.29-1.7-1.29-1.7-1.05-.72.08-.7.08-.7 1.17.08 1.78 1.2 1.78 1.2 1.04 1.78 2.72 1.27 3.38.97.11-.75.41-1.27.74-1.56-2.55-.29-5.24-1.28-5.24-5.71 0-1.26.45-2.29 1.19-3.1-.12-.29-.52-1.47.11-3.07 0 0 .98-.31 3.2 1.18a11.1 11.1 0 0 1 5.83 0c2.22-1.49 3.2-1.18 3.2-1.18.63 1.6.23 2.78.11 3.07.74.81 1.19 1.84 1.19 3.1 0 4.44-2.69 5.41-5.26 5.7.42.36.79 1.07.79 2.16v3.2c0 .31.21.67.79.55C20.21 21.39 23.5 17.08 23.5 12 23.5 5.65 18.35.5 12 .5z" /> 1434 </svg> 1435 <span>GitHub</span> 1436 </a> 1437 <a class="footer-link" href="https://ko-fi.com/s/1bd08ce885" target="_blank" rel="noopener"> 1438 <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" 1439 stroke-linejoin="round" width="13" height="13" aria-hidden> 1440 <path d="M4 9h13v6a4 4 0 0 1-4 4H8a4 4 0 0 1-4-4V9z" /> 1441 <path d="M17 11h2a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-2" /> 1442 <path d="M7 3v3M10 3v3M13 3v3" /> 1443 </svg> 1444 <span>Coffee</span> 1445 </a> 1446 <a class="footer-link" href="/cdn-cgi/l/email-protection#3f525a7f5c4d564c4b565e51585e5d4d565a53115b5a49"> 1447 <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" 1448 stroke-linejoin="round" width="13" height="13" aria-hidden> 1449 <rect x="3" y="5" width="18" height="14" rx="2" /> 1450 <path d="M3 7l9 6 9-6" /> 1451 </svg> 1452 <span>Contact</span> 1453 </a> 1454 <a class="footer-link mono" href="https://cristiangabriel.dev" target="_blank" rel="noopener"> 1455 <span>cristiangabriel.dev</span> 1456 <span class="footer-arrow" aria-hidden>â</span> 1457 </a> 1458 </div> 1459 </div> 1460 </footer>
vendor: 111 bytes, lines 1460-1462
1460 1461 1462 <script data-cfasync="false" src="/cdn-cgi/scripts/5c5dd728/cloudflare-static/email-decode.min.js"></script>
1462<script> 1463 // --- theme toggle --- 1464 const root = document.documentElement; 1465 const btn = document.getElementById("themeToggle"); 1466 const icon = document.getElementById("themeIcon"); 1467 const SUN = `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" width="15" height="15"><circle cx="12" cy="12" r="4"/><path d="M12 2v2M12 20v2M4.93 4.93l1.41 1.41M17.66 17.66l1.41 1.41M2 12h2M20 12h2M4.93 19.07l1.41-1.41M17.66 6.34l1.41-1.41"/></svg>`; 1468 const MOON = `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" width="15" height="15"><path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"/></svg>`; 1469 const apply = (mode) => { 1470 root.dataset.theme = mode; 1471 btn.innerHTML = mode === "dark" ? SUN : MOON; 1472 try { localStorage.setItem("tapedeck.presentation.theme", mode); } catch { } 1473 }; 1474 const initial = (() => { 1475 try { return localStorage.getItem("tapedeck.presentation.theme"); } catch { return null; } 1476 })() || "light"; 1477 apply(initial); 1478 btn.addEventListener("click", () => apply(root.dataset.theme === "dark" ? "light" : "dark")); 1479 1480 // --- reveal on scroll --- 1481 if ("IntersectionObserver" in window) { 1482 const io = new IntersectionObserver((entries) => { 1483 for (const e of entries) { 1484 if (e.isIntersecting) { e.target.classList.add("in"); io.unobserve(e.target); } 1485 } 1486 }, { rootMargin: "0px 0px -10% 0px", threshold: 0.05 }); 1487 document.querySelectorAll(".reveal").forEach((el) => io.observe(el)); 1488 } else { 1489 document.querySelectorAll(".reveal").forEach((el) => el.classList.add("in")); 1490 } 1491 </script>
1491 1492
1493<script type="module" src="https://static.cloudflareinsights.com/beacon.min.js/v31edd6df95cf4e85bb4c19e7a9bdbcba1788362987495" integrity="sha512-iIg7k2xntmwu6/uSb5tpc/hySgZc4eoL31yB29W6tJFo2akwjPWcEqnCEdJvGexCL0KEQwVYv5BlowfhVz26hg==" data-cf-beacon='{"version":"2024.11.0","token":"2acc8e9d290340e98d5f62d464ebdbdf","r":1,"spa":2}' crossorigin="anonymous"></script>
1493 1494</body> 1495 1496</html>
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.