PageSourceSearch

https://cristiangabriel.dev/tapedeck/tapedeck

html cristiangabriel.dev collected 2026-10-02 01:52:11 UTC 42,609 bytes, 1,496 lines download raw bytes

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 &amp; 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 &amp; 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&rsquo;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&rsquo;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.