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>