this repo has no description
1<!DOCTYPE html>
2<html lang="en">
3<head>
4 <meta charset="UTF-8">
5 <meta name="viewport" content="width=device-width, initial-scale=1.0">
6 <title>Tranquil</title>
7 <link rel="preconnect" href="https://fonts.googleapis.com">
8 <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
9 <link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;600;700;800&display=swap" rel="stylesheet">
10 <style>
11 * { margin: 0; padding: 0; box-sizing: border-box; }
12
13 :root {
14 --primary: #2c00ff;
15 --primary-dark: #1a00a3;
16 --primary-light: #4d33ff;
17 --primary-muted: #e8e5ff;
18 --secondary: #ff2400;
19 --secondary-hover: #ff5533;
20 --bg: #ffffff;
21 --bg-subtle: #f8f8fa;
22 --text: #1a1a1a;
23 --text-muted: #666666;
24 --text-light: #999999;
25 --border: #e5e5e5;
26 --border-light: #f0f0f0;
27 }
28
29 body {
30 font-family: 'JetBrains Mono', monospace;
31 line-height: 1.7;
32 background: var(--bg);
33 color: var(--text);
34 min-height: 100vh;
35 position: relative;
36 }
37
38 .pattern-container {
39 position: fixed;
40 top: -32px;
41 left: -32px;
42 right: -32px;
43 bottom: -32px;
44 pointer-events: none;
45 z-index: 1;
46 overflow: hidden;
47 }
48
49 .pattern {
50 position: absolute;
51 top: 0;
52 left: 0;
53 width: calc(100% + 500px);
54 height: 100%;
55 animation: drift 80s linear infinite;
56 }
57
58 .dot {
59 position: absolute;
60 width: 10px;
61 height: 10px;
62 background: rgba(0, 0, 0, 0.06);
63 border-radius: 50%;
64 transition: transform 0.04s linear;
65 }
66
67 .pattern-fade {
68 position: fixed;
69 top: 0;
70 left: 0;
71 right: 0;
72 bottom: 0;
73 background: linear-gradient(135deg, transparent 50%, var(--bg) 75%);
74 pointer-events: none;
75 z-index: 2;
76 }
77
78 @keyframes drift {
79 0% { transform: translateX(-500px); }
80 100% { transform: translateX(0); }
81 }
82
83 nav { z-index: 100; }
84 main { position: relative; z-index: 10; }
85 .site-footer { position: relative; z-index: 10; }
86
87 a { color: var(--secondary); text-decoration: none; }
88 a:hover { color: var(--secondary-hover); }
89
90 nav {
91 position: fixed;
92 top: 12px;
93 left: 32px;
94 right: 32px;
95 background: var(--primary);
96 padding: 10px 18px;
97 z-index: 100;
98 border-radius: 8px;
99 border: 1px solid rgba(0, 0, 0, 0.1);
100 display: flex;
101 justify-content: space-between;
102 align-items: center;
103 }
104
105 nav .brand {
106 font-weight: 600;
107 font-size: 1rem;
108 letter-spacing: 0.08em;
109 color: #ffffff;
110 text-transform: uppercase;
111 }
112
113 nav .nav-meta {
114 font-size: 0.85rem;
115 color: rgba(255, 255, 255, 0.7);
116 letter-spacing: 0.05em;
117 }
118
119 main {
120 max-width: 1000px;
121 margin: 0 auto;
122 padding: 72px 32px 80px;
123 }
124
125 .meta {
126 display: flex;
127 align-items: center;
128 gap: 16px;
129 margin-bottom: 32px;
130 font-size: 0.8rem;
131 font-weight: 500;
132 text-transform: uppercase;
133 letter-spacing: 0.1em;
134 }
135
136 .category {
137 color: #ffffff;
138 background: var(--primary);
139 padding: 4px 10px;
140 border-radius: 4px;
141 }
142
143 .read-time {
144 color: var(--text-muted);
145 }
146
147 h1 {
148 font-size: 2.75rem;
149 font-weight: 600;
150 line-height: 1.15;
151 color: var(--text);
152 margin-bottom: 32px;
153 letter-spacing: -0.02em;
154 }
155
156 .byline {
157 display: flex;
158 align-items: center;
159 gap: 16px;
160 padding: 24px 0;
161 border-top: 1px solid var(--border);
162 border-bottom: 1px solid var(--border);
163 margin-bottom: 48px;
164 }
165
166 .avatar {
167 width: 44px;
168 height: 44px;
169 border-radius: 50%;
170 background: linear-gradient(135deg, var(--secondary) 0%, #ff6b4a 100%);
171 }
172
173 .author-info {
174 flex: 1;
175 }
176
177 .author {
178 display: block;
179 font-weight: 500;
180 color: var(--text);
181 font-size: 1rem;
182 }
183
184 .author-handle {
185 display: block;
186 font-size: 0.85rem;
187 color: var(--text-muted);
188 margin-top: 2px;
189 }
190
191 .verification {
192 font-size: 0.75rem;
193 font-weight: 500;
194 color: var(--secondary);
195 text-transform: uppercase;
196 letter-spacing: 0.08em;
197 }
198
199 .placeholder-image {
200 aspect-ratio: 16 / 9;
201 background: var(--bg-subtle);
202 border-radius: 8px;
203 display: flex;
204 align-items: center;
205 justify-content: center;
206 font-size: 0.9rem;
207 color: var(--text-light);
208 text-transform: uppercase;
209 letter-spacing: 0.1em;
210 border: 1px solid var(--border);
211 }
212
213 figcaption {
214 margin-top: 12px;
215 font-size: 0.85rem;
216 color: var(--text-muted);
217 text-align: center;
218 }
219
220 .carousel {
221 margin: 64px 0 0;
222 }
223
224 .carousel-header {
225 display: flex;
226 justify-content: space-between;
227 align-items: center;
228 margin-bottom: 20px;
229 }
230
231 .carousel-title {
232 font-size: 0.85rem;
233 font-weight: 600;
234 text-transform: uppercase;
235 letter-spacing: 0.1em;
236 color: var(--text);
237 }
238
239 .carousel-nav {
240 display: flex;
241 gap: 8px;
242 }
243
244 .carousel-nav button {
245 font-family: 'JetBrains Mono', monospace;
246 width: 36px;
247 height: 36px;
248 background: var(--bg);
249 border: 1px solid var(--border);
250 border-radius: 6px;
251 color: var(--text);
252 cursor: pointer;
253 transition: all 0.15s ease;
254 font-size: 1rem;
255 }
256
257 .carousel-nav button:hover {
258 background: rgba(255, 36, 0, 0.08);
259 border-color: var(--secondary);
260 color: var(--secondary);
261 }
262
263 .carousel-track {
264 display: flex;
265 gap: 16px;
266 overflow-x: auto;
267 scroll-snap-type: x mandatory;
268 scrollbar-width: none;
269 -ms-overflow-style: none;
270 padding-bottom: 8px;
271 -webkit-overflow-scrolling: touch;
272 user-select: none;
273 }
274
275 .carousel-track::-webkit-scrollbar {
276 display: none;
277 }
278
279 .carousel-slide {
280 flex: 0 0 70%;
281 scroll-snap-align: start;
282 }
283
284 .carousel-slide .placeholder-image {
285 aspect-ratio: 16 / 10;
286 }
287
288 .carousel-label {
289 margin-top: 12px;
290 font-size: 0.8rem;
291 font-weight: 500;
292 color: var(--text-muted);
293 text-transform: uppercase;
294 letter-spacing: 0.08em;
295 }
296
297 .content {
298 font-size: 1.05rem;
299 font-weight: 400;
300 }
301
302 .content p {
303 margin-bottom: 28px;
304 }
305
306 .lede {
307 font-size: 1.3rem;
308 font-weight: 500;
309 color: var(--text);
310 line-height: 1.5;
311 }
312
313 .hero {
314 padding: 32px 0 40px;
315 border-bottom: 1px solid var(--border);
316 margin-bottom: 40px;
317 }
318
319 .content h2 {
320 font-size: 0.9rem;
321 font-weight: 600;
322 text-transform: uppercase;
323 letter-spacing: 0.1em;
324 color: var(--primary-dark);
325 margin: 56px 0 24px;
326 }
327
328 .content h2:first-child {
329 margin-top: 0;
330 }
331
332 .features {
333 display: grid;
334 grid-template-columns: repeat(2, 1fr);
335 gap: 32px;
336 margin: 32px 0 56px;
337 }
338
339 .feature {
340 padding: 24px;
341 background: var(--bg-subtle);
342 border-radius: 8px;
343 border: 1px solid var(--border);
344 }
345
346 .feature h3 {
347 font-size: 1rem;
348 font-weight: 600;
349 color: var(--text);
350 margin-bottom: 12px;
351 }
352
353 .feature p {
354 font-size: 0.95rem;
355 color: var(--text-muted);
356 margin-bottom: 0;
357 line-height: 1.6;
358 }
359
360 @media (max-width: 700px) {
361 .features {
362 grid-template-columns: 1fr;
363 }
364 }
365
366 blockquote {
367 margin: 40px 0;
368 padding: 32px;
369 background: var(--primary-muted);
370 border-left: 3px solid var(--primary);
371 border-radius: 0 8px 8px 0;
372 }
373
374 blockquote p {
375 font-size: 1.15rem;
376 color: var(--primary-dark);
377 font-style: italic;
378 margin-bottom: 16px !important;
379 }
380
381 blockquote cite {
382 font-size: 0.8rem;
383 color: var(--text-muted);
384 font-style: normal;
385 text-transform: uppercase;
386 letter-spacing: 0.05em;
387 }
388
389 .context-panel {
390 margin: 40px 0;
391 padding: 24px;
392 background: var(--bg-subtle);
393 border-radius: 8px;
394 border: 1px solid var(--border);
395 }
396
397 .context-panel h3 {
398 font-size: 0.8rem;
399 font-weight: 600;
400 text-transform: uppercase;
401 letter-spacing: 0.1em;
402 color: var(--text);
403 margin-bottom: 16px;
404 }
405
406 .context-panel ul {
407 list-style: none;
408 }
409
410 .context-panel li {
411 padding: 10px 0;
412 border-bottom: 1px solid var(--border-light);
413 }
414
415 .context-panel li:last-child {
416 border-bottom: none;
417 }
418
419 .context-panel a {
420 font-size: 0.95rem;
421 font-weight: 500;
422 color: var(--secondary);
423 text-decoration: none;
424 transition: color 0.15s ease;
425 }
426
427 .context-panel a:hover {
428 color: var(--secondary-hover);
429 }
430
431 .article-footer {
432 margin-top: 64px;
433 padding-top: 32px;
434 border-top: 1px solid var(--border);
435 }
436
437 .actions {
438 display: flex;
439 gap: 12px;
440 margin-bottom: 24px;
441 }
442
443 .actions button {
444 font-family: 'JetBrains Mono', monospace;
445 font-size: 0.85rem;
446 font-weight: 500;
447 text-transform: uppercase;
448 letter-spacing: 0.06em;
449 padding: 14px 24px;
450 background: var(--bg);
451 border: 1px solid var(--border);
452 border-radius: 6px;
453 color: var(--text);
454 cursor: pointer;
455 transition: all 0.15s ease;
456 }
457
458 .actions button:hover {
459 background: rgba(255, 36, 0, 0.08);
460 border-color: var(--secondary);
461 color: var(--secondary);
462 }
463
464 .actions button:first-child {
465 background: var(--secondary);
466 border-color: var(--secondary);
467 color: #ffffff;
468 }
469
470 .actions button:first-child:hover {
471 background: #cc1d00;
472 border-color: #cc1d00;
473 }
474
475 .attestation-info {
476 display: flex;
477 flex-wrap: wrap;
478 gap: 24px;
479 font-size: 0.8rem;
480 color: var(--text-light);
481 text-transform: uppercase;
482 letter-spacing: 0.05em;
483 }
484
485 .site-footer {
486 max-width: 1000px;
487 margin: 0 auto;
488 padding: 48px 32px;
489 display: flex;
490 justify-content: space-between;
491 font-size: 0.8rem;
492 color: var(--text-light);
493 text-transform: uppercase;
494 letter-spacing: 0.05em;
495 border-top: 1px solid var(--border);
496 }
497
498 ::selection {
499 background: rgba(255, 36, 0, 0.2);
500 color: var(--text);
501 }
502 </style>
503</head>
504<body>
505
506<div class="pattern-container">
507 <div class="pattern"></div>
508</div>
509<div class="pattern-fade"></div>
510
511<nav>
512 <span class="brand">Tranquil PDS</span>
513 <span class="nav-meta">0.1.0</span>
514</nav>
515
516<main>
517 <section class="hero">
518 <h1>A home for your ATProto account</h1>
519
520 <p class="lede">Tranquil PDS is a Personal Data Server, the thing that stores your posts, profile, and keys. Bluesky runs one for you, but you can run your own.</p>
521
522 <div class="actions" style="margin-top: 40px; margin-bottom: 0;">
523 <button>Join This Server</button>
524 <button>Run Your Own</button>
525 </div>
526 <blockquote>
527 <p>"Nature does not hurry, yet everything is accomplished."</p>
528 <cite>Lao Tzu</cite>
529 </blockquote>
530 </section>
531
532 <section class="content">
533 <h2>What you get</h2>
534
535 <div class="features">
536 <div class="feature">
537 <h3>Real security</h3>
538 <p>Sign in with passkeys, add two-factor authentication, set up backup codes, and mark devices you trust. Your account stays yours.</p>
539 </div>
540
541 <div class="feature">
542 <h3>Your own identity</h3>
543 <p>Use your own domain as your handle, or get a subdomain on ours. Either way, your identity moves with you if you ever leave.</p>
544 </div>
545
546 <div class="feature">
547 <h3>Stay in the loop</h3>
548 <p>Get important alerts where you actually see them: email, Discord, Telegram, or Signal.</p>
549 </div>
550
551 <div class="feature">
552 <h3>You decide what apps can do</h3>
553 <p>When an app asks for access, you'll see exactly what it wants in plain language. Grant what makes sense, deny what doesn't.</p>
554 </div>
555 </div>
556
557 <h2>Everything in one place</h2>
558
559 <p>Manage your profile, security settings, connected apps, and more from a clean dashboard. No command line or 3rd party apps required.</p>
560
561 <div class="carousel">
562 <div class="carousel-header">
563 <span class="carousel-title">Interface</span>
564 <div class="carousel-nav">
565 <button class="carousel-prev">←</button>
566 <button class="carousel-next">→</button>
567 </div>
568 </div>
569 <div class="carousel-track">
570 <div class="carousel-slide">
571 <div class="placeholder-image">Dashboard</div>
572 <div class="carousel-label">Dashboard</div>
573 </div>
574 <div class="carousel-slide">
575 <div class="placeholder-image">Profile Settings</div>
576 <div class="carousel-label">Profile Settings</div>
577 </div>
578 <div class="carousel-slide">
579 <div class="placeholder-image">Account Security</div>
580 <div class="carousel-label">Account Security</div>
581 </div>
582 <div class="carousel-slide">
583 <div class="placeholder-image">Connected Apps</div>
584 <div class="carousel-label">Connected Apps</div>
585 </div>
586 <div class="carousel-slide">
587 <div class="placeholder-image">Invite Friends</div>
588 <div class="carousel-label">Invite Friends</div>
589 </div>
590 </div>
591 </div>
592
593 <h2>Works with everything</h2>
594
595 <p>Use any ATProto app you already like. Tranquil PDS speaks the same language as Bluesky's servers, so all your favorite clients, tools, and bots just work.</p>
596
597 <h2>Ready to try it?</h2>
598
599 <p>Join this server, or grab the source and run your own. Either way, you can migrate an existing account over and your followers, posts, and identity come with you.</p>
600
601 <div class="actions" style="margin-top: 32px;">
602 <button>Join This Server</button>
603 <button>View Source</button>
604 </div>
605 </section>
606</main>
607
608<footer class="site-footer">
609 <div>Open Source</div>
610 <div>Made with care</div>
611</footer>
612
613<script>
614const pattern = document.querySelector('.pattern');
615const spacing = 32;
616const cols = Math.ceil((window.innerWidth + 600) / spacing);
617const rows = Math.ceil((window.innerHeight + 100) / spacing);
618const dots = [];
619
620for (let y = 0; y < rows; y++) {
621 for (let x = 0; x < cols; x++) {
622 const dot = document.createElement('div');
623 dot.className = 'dot';
624 dot.style.left = (x * spacing) + 'px';
625 dot.style.top = (y * spacing) + 'px';
626 pattern.appendChild(dot);
627 dots.push({ el: dot, x: x * spacing, y: y * spacing });
628 }
629}
630
631let mouseX = -1000, mouseY = -1000;
632document.addEventListener('mousemove', e => {
633 mouseX = e.clientX;
634 mouseY = e.clientY;
635});
636
637function updateDots() {
638 const patternRect = pattern.getBoundingClientRect();
639 dots.forEach(dot => {
640 const dotX = patternRect.left + dot.x + 5;
641 const dotY = patternRect.top + dot.y + 5;
642 const dist = Math.hypot(mouseX - dotX, mouseY - dotY);
643 const maxDist = 120;
644 const scale = Math.min(1, Math.max(0.1, dist / maxDist));
645 dot.el.style.transform = `scale(${scale})`;
646 });
647 requestAnimationFrame(updateDots);
648}
649updateDots();
650
651const track = document.querySelector('.carousel-track');
652const prevBtn = document.querySelector('.carousel-prev');
653const nextBtn = document.querySelector('.carousel-next');
654const slideWidth = track?.querySelector('.carousel-slide')?.offsetWidth + 16;
655
656prevBtn?.addEventListener('click', () => {
657 track.scrollBy({ left: -slideWidth, behavior: 'smooth' });
658});
659nextBtn?.addEventListener('click', () => {
660 track.scrollBy({ left: slideWidth, behavior: 'smooth' });
661});
662
663let isDragging = false;
664let startX, scrollLeft;
665
666track?.addEventListener('mousedown', e => {
667 isDragging = true;
668 track.style.cursor = 'grabbing';
669 track.style.scrollSnapType = 'none';
670 startX = e.pageX - track.offsetLeft;
671 scrollLeft = track.scrollLeft;
672});
673
674track?.addEventListener('mouseleave', () => {
675 isDragging = false;
676 track.style.cursor = 'grab';
677 track.style.scrollSnapType = 'x mandatory';
678});
679
680function snapTo(target, duration = 120) {
681 const start = track.scrollLeft;
682 const distance = target - start;
683 const startTime = performance.now();
684 function step(currentTime) {
685 const elapsed = currentTime - startTime;
686 const progress = Math.min(elapsed / duration, 1);
687 const ease = 1 - Math.pow(1 - progress, 3);
688 track.scrollLeft = start + distance * ease;
689 if (progress < 1) requestAnimationFrame(step);
690 else track.style.scrollSnapType = 'x mandatory';
691 }
692 requestAnimationFrame(step);
693}
694
695track?.addEventListener('mouseup', () => {
696 isDragging = false;
697 track.style.cursor = 'grab';
698 const slideW = track.querySelector('.carousel-slide').offsetWidth + 16;
699 const targetIndex = Math.round(track.scrollLeft / slideW);
700 snapTo(targetIndex * slideW);
701});
702
703track?.addEventListener('mousemove', e => {
704 if (!isDragging) return;
705 e.preventDefault();
706 const x = e.pageX - track.offsetLeft;
707 const walk = (x - startX) * 1.5;
708 track.scrollLeft = scrollLeft - walk;
709});
710
711if (track) track.style.cursor = 'grab';
712</script>
713</body>
714</html>