Write on the margins of the internet. Powered by the AT Protocol. margin.at
extension web atproto comments

Landing Page

+1705 -25
+21 -19
web/index.html
··· 1 1 <!doctype html> 2 2 <html lang="en"> 3 - 4 - <head> 5 - <meta charset="UTF-8" /> 6 - <link rel="icon" href="/favicon.ico" /> 7 - <meta name="viewport" content="width=device-width, initial-scale=1.0" /> 8 - <meta name="description" content="Margin - Write in the margins of the web. Comment on any URL with AT Protocol." /> 9 - <title>Margin - Write in the margins of the web</title> 10 - <link rel="preconnect" href="https://fonts.googleapis.com" /> 11 - <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin /> 12 - <link 13 - href="https://fonts.googleapis.com/css2?family=Instrument+Sans:ital,wght@0,400..700;1,400..700&family=Plus+Jakarta+Sans:ital,wght@0,200..800;1,200..800&display=swap" 14 - rel="stylesheet" /> 15 - </head> 16 - 17 - <body> 18 - <div id="root"></div> 19 - <script type="module" src="/src/main.jsx"></script> 20 - </body> 3 + <head> 4 + <meta charset="UTF-8" /> 5 + <link rel="icon" href="/favicon.ico" /> 6 + <meta name="viewport" content="width=device-width, initial-scale=1.0" /> 7 + <meta 8 + name="description" 9 + content="Margin - Write in the margins of the web. Comment on any URL with AT Protocol." 10 + /> 11 + <title>Margin - Write in the margins of the web</title> 12 + <link rel="preconnect" href="https://fonts.googleapis.com" /> 13 + <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin /> 14 + <link 15 + href="https://fonts.googleapis.com/css2?family=Instrument+Sans:ital,wght@0,400..700;1,400..700&family=Plus+Jakarta+Sans:ital,wght@0,200..800;1,200..800&display=swap" 16 + rel="stylesheet" 17 + /> 18 + </head> 21 19 22 - </html> 20 + <body> 21 + <div id="root"></div> 22 + <script type="module" src="/src/main.jsx"></script> 23 + </body> 24 + </html>
+11
web/package-lock.json
··· 8 8 "name": "margin-web", 9 9 "version": "0.0.1", 10 10 "dependencies": { 11 + "date-fns": "^4.1.0", 11 12 "lucide-react": "^0.562.0", 12 13 "react": "^18.3.1", 13 14 "react-dom": "^18.3.1", ··· 1941 1942 }, 1942 1943 "funding": { 1943 1944 "url": "https://github.com/sponsors/ljharb" 1945 + } 1946 + }, 1947 + "node_modules/date-fns": { 1948 + "version": "4.1.0", 1949 + "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-4.1.0.tgz", 1950 + "integrity": "sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==", 1951 + "license": "MIT", 1952 + "funding": { 1953 + "type": "github", 1954 + "url": "https://github.com/sponsors/kossnocorp" 1944 1955 } 1945 1956 }, 1946 1957 "node_modules/debug": {
+1
web/package.json
··· 10 10 "preview": "vite preview" 11 11 }, 12 12 "dependencies": { 13 + "date-fns": "^4.1.0", 13 14 "lucide-react": "^0.562.0", 14 15 "react": "^18.3.1", 15 16 "react-dom": "^18.3.1",
+3 -1
web/src/App.jsx
··· 17 17 import CollectionDetail from "./pages/CollectionDetail"; 18 18 import Privacy from "./pages/Privacy"; 19 19 import Terms from "./pages/Terms"; 20 + import Landing from "./pages/Landing"; 20 21 import ScrollToTop from "./components/ScrollToTop"; 21 22 import { ThemeProvider } from "./context/ThemeContext"; 22 23 ··· 35 36 <TopNav /> 36 37 <main className="main-content"> 37 38 <Routes> 38 - <Route path="/" element={<Feed />} /> 39 + <Route path="/home" element={<Feed />} /> 39 40 <Route path="/url" element={<Url />} /> 40 41 <Route path="/new" element={<New />} /> 41 42 <Route path="/bookmarks" element={<Bookmarks />} /> ··· 80 81 <ThemeProvider> 81 82 <AuthProvider> 82 83 <Routes> 84 + <Route path="/" element={<Landing />} /> 83 85 <Route path="/*" element={<AppContent />} /> 84 86 </Routes> 85 87 </AuthProvider>
+5 -5
web/src/components/TopNav.jsx
··· 123 123 return ( 124 124 <header className="top-nav"> 125 125 <div className="top-nav-inner"> 126 - <Link to="/" className="top-nav-logo"> 126 + <Link to="/home" className="top-nav-logo"> 127 127 <img src={logo} alt="Margin" /> 128 128 <span>Margin</span> 129 129 </Link> 130 130 131 131 <nav className="top-nav-links"> 132 132 <Link 133 - to="/" 134 - className={`top-nav-link ${isActive("/") ? "active" : ""}`} 133 + to="/home" 134 + className={`top-nav-link ${isActive("/home") ? "active" : ""}`} 135 135 > 136 136 Home 137 137 </Link> ··· 337 337 {mobileMenuOpen && ( 338 338 <div className="mobile-menu"> 339 339 <Link 340 - to="/" 341 - className={`mobile-menu-link ${isActive("/") ? "active" : ""}`} 340 + to="/home" 341 + className={`mobile-menu-link ${isActive("/home") ? "active" : ""}`} 342 342 onClick={closeMobileMenu} 343 343 > 344 344 <Home size={20} /> Home
+925
web/src/css/landing.css
··· 1 + .landing-page { 2 + min-height: 100vh; 3 + background: var(--bg-primary); 4 + } 5 + 6 + .landing-nav { 7 + display: flex; 8 + justify-content: space-between; 9 + align-items: center; 10 + padding: 16px 32px; 11 + max-width: 1200px; 12 + margin: 0 auto; 13 + } 14 + 15 + .landing-logo { 16 + display: flex; 17 + align-items: center; 18 + gap: 10px; 19 + text-decoration: none; 20 + color: var(--text-primary); 21 + font-weight: 600; 22 + font-size: 1.1rem; 23 + } 24 + 25 + .landing-logo img { 26 + width: 28px; 27 + height: 28px; 28 + } 29 + 30 + .landing-nav-links { 31 + display: flex; 32 + align-items: center; 33 + gap: 24px; 34 + } 35 + 36 + .landing-nav-links a:not(.btn) { 37 + color: var(--text-secondary); 38 + text-decoration: none; 39 + font-size: 0.9rem; 40 + transition: color 0.15s; 41 + } 42 + 43 + .landing-nav-links a:not(.btn):hover { 44 + color: var(--text-primary); 45 + } 46 + 47 + .landing-hero { 48 + padding: 80px 32px 40px; 49 + max-width: 800px; 50 + margin: 0 auto; 51 + text-align: center; 52 + } 53 + 54 + .landing-hero-content { 55 + display: flex; 56 + flex-direction: column; 57 + align-items: center; 58 + gap: 24px; 59 + } 60 + 61 + .landing-badge { 62 + display: inline-flex; 63 + align-items: center; 64 + gap: 8px; 65 + font-size: 0.8rem; 66 + font-weight: 500; 67 + color: var(--accent); 68 + background: var(--accent-subtle); 69 + padding: 6px 14px; 70 + border-radius: var(--radius-full); 71 + } 72 + 73 + .landing-title { 74 + font-size: 3.5rem; 75 + font-weight: 700; 76 + line-height: 1.1; 77 + letter-spacing: -0.03em; 78 + color: var(--text-primary); 79 + margin: 0; 80 + } 81 + 82 + .landing-title-accent { 83 + color: var(--accent); 84 + } 85 + 86 + .landing-subtitle { 87 + font-size: 1.2rem; 88 + line-height: 1.7; 89 + color: var(--text-secondary); 90 + max-width: 580px; 91 + margin: 0; 92 + } 93 + 94 + .landing-cta { 95 + display: flex; 96 + gap: 12px; 97 + flex-wrap: wrap; 98 + justify-content: center; 99 + margin-top: 8px; 100 + } 101 + 102 + .btn-lg { 103 + padding: 10px 20px; 104 + font-size: 0.95rem; 105 + } 106 + 107 + .landing-browsers { 108 + font-size: 0.85rem; 109 + color: var(--text-tertiary); 110 + margin: 0; 111 + } 112 + 113 + .landing-browsers a { 114 + color: var(--text-secondary); 115 + text-decoration: underline; 116 + text-underline-offset: 2px; 117 + } 118 + 119 + .landing-browsers a:hover { 120 + color: var(--text-primary); 121 + } 122 + 123 + .landing-demo { 124 + padding: 40px 32px 80px; 125 + max-width: 1100px; 126 + margin: 0 auto; 127 + } 128 + 129 + .demo-window { 130 + background: var(--bg-secondary); 131 + border: 1px solid var(--border); 132 + border-radius: var(--radius-xl); 133 + overflow: hidden; 134 + box-shadow: var(--shadow-lg); 135 + } 136 + 137 + .demo-browser-bar { 138 + display: flex; 139 + align-items: center; 140 + gap: 16px; 141 + padding: 12px 16px; 142 + background: var(--bg-tertiary); 143 + border-bottom: 1px solid var(--border); 144 + } 145 + 146 + .demo-browser-dots { 147 + display: flex; 148 + gap: 6px; 149 + } 150 + 151 + .demo-browser-dots span { 152 + width: 12px; 153 + height: 12px; 154 + border-radius: 50%; 155 + background: var(--border); 156 + } 157 + 158 + .demo-browser-url { 159 + flex: 1; 160 + background: var(--bg-primary); 161 + border-radius: var(--radius-md); 162 + padding: 8px 14px; 163 + font-size: 0.8rem; 164 + color: var(--text-tertiary); 165 + } 166 + 167 + .demo-content { 168 + display: grid; 169 + grid-template-columns: 1fr 340px; 170 + min-height: 380px; 171 + } 172 + 173 + .demo-article { 174 + padding: 32px; 175 + border-right: 1px solid var(--border); 176 + } 177 + 178 + .demo-text { 179 + font-size: 1.05rem; 180 + line-height: 1.9; 181 + color: var(--text-primary); 182 + margin: 0 0 20px 0; 183 + } 184 + 185 + .demo-text:last-child { 186 + margin-bottom: 0; 187 + } 188 + 189 + .demo-highlight { 190 + background-color: transparent; 191 + color: inherit; 192 + border-bottom: 2px solid var(--accent); 193 + } 194 + 195 + .demo-sidebar { 196 + padding: 0; 197 + background: var(--bg-primary); 198 + display: flex; 199 + flex-direction: column; 200 + gap: 0; 201 + overflow-y: auto; 202 + font-family: 203 + "IBM Plex Sans", 204 + -apple-system, 205 + BlinkMacSystemFont, 206 + sans-serif; 207 + } 208 + 209 + .demo-sidebar-header { 210 + display: flex; 211 + align-items: center; 212 + justify-content: space-between; 213 + padding: 14px 16px; 214 + border-bottom: 1px solid var(--border); 215 + background: var(--bg-primary); 216 + } 217 + 218 + .demo-logo-section { 219 + display: flex; 220 + align-items: center; 221 + gap: 10px; 222 + } 223 + 224 + .demo-logo-icon { 225 + color: var(--accent); 226 + display: flex; 227 + align-items: center; 228 + } 229 + 230 + .demo-logo-text { 231 + font-weight: 600; 232 + font-size: 15px; 233 + color: var(--text-primary); 234 + letter-spacing: -0.02em; 235 + } 236 + 237 + .demo-user-section { 238 + display: flex; 239 + align-items: center; 240 + gap: 8px; 241 + } 242 + 243 + .demo-user-handle { 244 + font-size: 12px; 245 + color: var(--text-secondary); 246 + background: var(--bg-tertiary); 247 + padding: 4px 10px; 248 + border-radius: 9999px; 249 + } 250 + 251 + .demo-user-avatar { 252 + width: 24px; 253 + height: 24px; 254 + border-radius: 50%; 255 + background: var(--bg-hover); 256 + color: var(--text-secondary); 257 + display: flex; 258 + align-items: center; 259 + justify-content: center; 260 + font-size: 12px; 261 + font-weight: 600; 262 + } 263 + 264 + .demo-page-info { 265 + display: flex; 266 + align-items: center; 267 + gap: 8px; 268 + padding: 10px 16px; 269 + background: var(--bg-primary); 270 + border-bottom: 1px solid var(--border); 271 + font-size: 12px; 272 + color: var(--text-tertiary); 273 + } 274 + 275 + .demo-annotations-list { 276 + display: flex; 277 + flex-direction: column; 278 + gap: 1px; 279 + background: var(--border); 280 + } 281 + 282 + .demo-annotation { 283 + background: var(--bg-primary); 284 + border: none; 285 + border-radius: 0; 286 + padding: 14px 16px; 287 + } 288 + 289 + .demo-annotation-secondary { 290 + opacity: 1; 291 + } 292 + 293 + .demo-annotation-header { 294 + display: flex; 295 + align-items: center; 296 + gap: 10px; 297 + margin-bottom: 8px; 298 + } 299 + 300 + .demo-avatar { 301 + width: 26px; 302 + height: 26px; 303 + border-radius: 50%; 304 + background: var(--accent); 305 + color: var(--bg-primary); 306 + display: flex; 307 + align-items: center; 308 + justify-content: center; 309 + font-size: 10px; 310 + font-weight: 600; 311 + } 312 + 313 + .demo-meta { 314 + display: flex; 315 + flex-direction: column; 316 + gap: 0; 317 + } 318 + 319 + .demo-author { 320 + font-size: 12px; 321 + font-weight: 600; 322 + color: var(--text-primary); 323 + } 324 + 325 + .demo-time { 326 + font-size: 11px; 327 + color: var(--text-tertiary); 328 + } 329 + 330 + .demo-quote { 331 + font-size: 12px; 332 + font-style: italic; 333 + color: var(--text-secondary); 334 + padding: 8px 12px; 335 + border-left: 2px solid var(--accent); 336 + margin: 0 0 8px 0; 337 + background: var(--accent-subtle); 338 + border-radius: 0 6px 6px 0; 339 + line-height: 1.5; 340 + } 341 + 342 + .demo-comment { 343 + font-size: 13px; 344 + line-height: 1.5; 345 + color: var(--text-primary); 346 + margin: 0 0 12px 0; 347 + } 348 + 349 + .demo-jump-btn { 350 + background: transparent; 351 + border: none; 352 + padding: 0; 353 + color: var(--accent); 354 + font-size: 11px; 355 + font-weight: 500; 356 + cursor: pointer; 357 + display: inline-flex; 358 + align-items: center; 359 + margin-top: 4px; 360 + } 361 + 362 + .demo-jump-btn:hover { 363 + text-decoration: underline; 364 + text-underline-offset: 2px; 365 + } 366 + 367 + .landing-section { 368 + padding: 80px 32px; 369 + max-width: 1000px; 370 + margin: 0 auto; 371 + } 372 + 373 + .landing-section-alt { 374 + background: var(--bg-secondary); 375 + max-width: none; 376 + } 377 + 378 + .landing-section-alt > * { 379 + max-width: 1000px; 380 + margin-left: auto; 381 + margin-right: auto; 382 + } 383 + 384 + .landing-section-title { 385 + font-size: 2rem; 386 + font-weight: 700; 387 + text-align: center; 388 + margin: 0 0 48px 0; 389 + color: var(--text-primary); 390 + } 391 + 392 + .landing-steps { 393 + display: flex; 394 + flex-direction: column; 395 + gap: 32px; 396 + } 397 + 398 + .landing-step { 399 + display: flex; 400 + gap: 24px; 401 + align-items: flex-start; 402 + } 403 + 404 + .landing-step-num { 405 + width: 40px; 406 + height: 40px; 407 + border-radius: 50%; 408 + background: var(--accent); 409 + color: white; 410 + display: flex; 411 + align-items: center; 412 + justify-content: center; 413 + font-weight: 700; 414 + font-size: 1.1rem; 415 + flex-shrink: 0; 416 + } 417 + 418 + .landing-step-content h3 { 419 + font-size: 1.15rem; 420 + font-weight: 600; 421 + margin: 0 0 8px 0; 422 + color: var(--text-primary); 423 + } 424 + 425 + .landing-step-content p { 426 + font-size: 1rem; 427 + color: var(--text-secondary); 428 + margin: 0; 429 + line-height: 1.6; 430 + } 431 + 432 + .landing-features-grid { 433 + display: grid; 434 + grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); 435 + gap: 32px; 436 + } 437 + 438 + .landing-feature { 439 + text-align: center; 440 + padding: 24px 16px; 441 + } 442 + 443 + .landing-feature-icon { 444 + width: 52px; 445 + height: 52px; 446 + border-radius: var(--radius-lg); 447 + background: var(--accent-subtle); 448 + color: var(--accent); 449 + display: flex; 450 + align-items: center; 451 + justify-content: center; 452 + margin: 0 auto 16px; 453 + } 454 + 455 + .landing-feature h3 { 456 + font-size: 1.05rem; 457 + font-weight: 600; 458 + margin: 0 0 8px 0; 459 + color: var(--text-primary); 460 + } 461 + 462 + .landing-feature p { 463 + font-size: 0.9rem; 464 + color: var(--text-secondary); 465 + margin: 0; 466 + line-height: 1.6; 467 + } 468 + 469 + .landing-protocol { 470 + background: var(--bg-secondary); 471 + max-width: none; 472 + border-top: 1px solid var(--border); 473 + border-bottom: 1px solid var(--border); 474 + } 475 + 476 + .landing-protocol-grid { 477 + display: grid; 478 + grid-template-columns: 1fr 1fr; 479 + gap: 64px; 480 + align-items: center; 481 + max-width: 1000px; 482 + margin: 0 auto; 483 + } 484 + 485 + .landing-protocol-main h2 { 486 + font-size: 1.75rem; 487 + font-weight: 700; 488 + margin: 0 0 16px 0; 489 + color: var(--text-primary); 490 + } 491 + 492 + .landing-protocol-main p { 493 + font-size: 1rem; 494 + color: var(--text-secondary); 495 + margin: 0 0 16px 0; 496 + line-height: 1.7; 497 + } 498 + 499 + .landing-protocol-main a { 500 + color: var(--accent); 501 + text-decoration: underline; 502 + text-underline-offset: 2px; 503 + } 504 + 505 + .landing-protocol-features { 506 + display: flex; 507 + flex-direction: column; 508 + gap: 20px; 509 + } 510 + 511 + .landing-protocol-item { 512 + display: flex; 513 + gap: 16px; 514 + align-items: flex-start; 515 + color: var(--accent); 516 + } 517 + 518 + .landing-protocol-item div { 519 + display: flex; 520 + flex-direction: column; 521 + } 522 + 523 + .landing-protocol-item strong { 524 + font-size: 0.95rem; 525 + font-weight: 600; 526 + color: var(--text-primary); 527 + } 528 + 529 + .landing-protocol-item span { 530 + font-size: 0.85rem; 531 + color: var(--text-tertiary); 532 + } 533 + 534 + .landing-final-cta { 535 + text-align: center; 536 + } 537 + 538 + .landing-final-cta h2 { 539 + font-size: 2rem; 540 + font-weight: 700; 541 + margin: 0 0 12px 0; 542 + color: var(--text-primary); 543 + } 544 + 545 + .landing-final-cta p { 546 + font-size: 1.1rem; 547 + color: var(--text-secondary); 548 + margin: 0 0 28px 0; 549 + } 550 + 551 + .landing-footer { 552 + border-top: 1px solid var(--border); 553 + padding: 48px 32px 32px; 554 + } 555 + 556 + .landing-footer-grid { 557 + display: flex; 558 + justify-content: space-between; 559 + max-width: 1000px; 560 + margin: 0 auto 40px; 561 + } 562 + 563 + .landing-footer-brand { 564 + max-width: 280px; 565 + } 566 + 567 + .landing-footer-brand p { 568 + font-size: 0.9rem; 569 + color: var(--text-tertiary); 570 + margin: 12px 0 0 0; 571 + } 572 + 573 + .landing-footer-links { 574 + display: flex; 575 + gap: 64px; 576 + } 577 + 578 + .landing-footer-col { 579 + display: flex; 580 + flex-direction: column; 581 + gap: 10px; 582 + } 583 + 584 + .landing-footer-col h4 { 585 + font-size: 0.75rem; 586 + font-weight: 600; 587 + text-transform: uppercase; 588 + letter-spacing: 0.08em; 589 + color: var(--text-tertiary); 590 + margin: 0 0 4px 0; 591 + } 592 + 593 + .landing-footer-col a { 594 + font-size: 0.9rem; 595 + color: var(--text-secondary); 596 + text-decoration: none; 597 + } 598 + 599 + .landing-footer-col a:hover { 600 + color: var(--text-primary); 601 + } 602 + 603 + .landing-footer-bottom { 604 + text-align: center; 605 + padding-top: 24px; 606 + border-top: 1px solid var(--border); 607 + max-width: 1000px; 608 + margin: 0 auto; 609 + } 610 + 611 + .landing-footer-bottom p { 612 + font-size: 0.85rem; 613 + color: var(--text-tertiary); 614 + margin: 0; 615 + } 616 + 617 + @media (max-width: 900px) { 618 + .demo-content { 619 + grid-template-columns: 1fr; 620 + } 621 + 622 + .demo-article { 623 + border-right: none; 624 + border-bottom: 1px solid var(--border); 625 + } 626 + 627 + .demo-sidebar { 628 + max-height: 340px; 629 + } 630 + 631 + .landing-protocol-grid { 632 + grid-template-columns: 1fr; 633 + gap: 40px; 634 + } 635 + } 636 + 637 + @media (max-width: 768px) { 638 + .landing-nav { 639 + padding: 16px 20px; 640 + } 641 + 642 + .landing-nav-links a:not(.btn) { 643 + display: none; 644 + } 645 + 646 + .landing-hero { 647 + padding: 60px 20px 30px; 648 + } 649 + 650 + .landing-title { 651 + font-size: 2.5rem; 652 + } 653 + 654 + .landing-subtitle { 655 + font-size: 1.1rem; 656 + } 657 + 658 + .landing-cta { 659 + flex-direction: column; 660 + width: 100%; 661 + } 662 + 663 + .landing-cta .btn { 664 + width: 100%; 665 + justify-content: center; 666 + } 667 + 668 + .landing-demo { 669 + padding: 30px 16px 60px; 670 + } 671 + 672 + .demo-browser-bar { 673 + padding: 10px 12px; 674 + } 675 + 676 + .demo-browser-dots { 677 + display: none; 678 + } 679 + 680 + .demo-article { 681 + padding: 20px; 682 + } 683 + 684 + .demo-text { 685 + font-size: 0.95rem; 686 + } 687 + 688 + .demo-sidebar { 689 + padding: 16px; 690 + } 691 + 692 + .landing-section { 693 + padding: 60px 20px; 694 + } 695 + 696 + .landing-section-title { 697 + font-size: 1.5rem; 698 + margin-bottom: 32px; 699 + } 700 + 701 + .landing-step { 702 + gap: 16px; 703 + } 704 + 705 + .landing-step-num { 706 + width: 32px; 707 + height: 32px; 708 + font-size: 0.95rem; 709 + } 710 + 711 + .landing-features-grid { 712 + grid-template-columns: 1fr; 713 + gap: 24px; 714 + } 715 + 716 + .landing-feature { 717 + text-align: left; 718 + display: flex; 719 + gap: 16px; 720 + padding: 16px 0; 721 + } 722 + 723 + .landing-feature-icon { 724 + margin: 0; 725 + width: 44px; 726 + height: 44px; 727 + flex-shrink: 0; 728 + } 729 + 730 + .landing-protocol-main h2 { 731 + font-size: 1.5rem; 732 + } 733 + 734 + .landing-footer { 735 + padding: 40px 20px 24px; 736 + } 737 + 738 + .landing-footer-grid { 739 + flex-direction: column; 740 + gap: 40px; 741 + } 742 + 743 + .landing-footer-links { 744 + flex-wrap: wrap; 745 + gap: 32px; 746 + } 747 + } 748 + 749 + .demo-hover-indicator { 750 + position: absolute; 751 + display: flex; 752 + align-items: center; 753 + z-index: 100; 754 + pointer-events: none; 755 + background: transparent; 756 + opacity: 0; 757 + transform: scale(0.8); 758 + transition: 759 + opacity 0.15s ease-out, 760 + transform 0.15s ease-out; 761 + } 762 + 763 + .demo-hover-indicator.visible { 764 + opacity: 1; 765 + transform: scale(1); 766 + } 767 + 768 + .demo-hover-avatar { 769 + width: 28px; 770 + height: 28px; 771 + border-radius: 50%; 772 + object-fit: cover; 773 + border: 2px solid var(--bg-primary); 774 + margin-left: -10px; 775 + background: var(--bg-elevated); 776 + } 777 + 778 + .demo-hover-avatar:first-child { 779 + margin-left: 0; 780 + } 781 + 782 + .demo-hover-avatar-fallback { 783 + width: 28px; 784 + height: 28px; 785 + border-radius: 50%; 786 + background: #6366f1; 787 + color: white; 788 + display: flex; 789 + align-items: center; 790 + justify-content: center; 791 + font-size: 12px; 792 + font-weight: 600; 793 + font-family: -apple-system, sans-serif; 794 + border: 2px solid var(--bg-primary); 795 + margin-left: -10px; 796 + } 797 + 798 + .demo-hover-avatar-fallback:first-child { 799 + margin-left: 0; 800 + } 801 + 802 + @keyframes demo-popover-in { 803 + from { 804 + opacity: 0; 805 + transform: translateY(-4px); 806 + } 807 + 808 + to { 809 + opacity: 1; 810 + transform: translateY(0); 811 + } 812 + } 813 + 814 + .demo-popover { 815 + position: absolute; 816 + width: 300px; 817 + background: var(--bg-card); 818 + border: 1px solid var(--border); 819 + border-radius: 12px; 820 + padding: 0; 821 + box-shadow: var(--shadow-lg); 822 + display: flex; 823 + flex-direction: column; 824 + z-index: 200; 825 + font-family: inherit; 826 + color: var(--text-primary); 827 + opacity: 0; 828 + animation: demo-popover-in 0.15s forwards; 829 + max-height: 400px; 830 + overflow: hidden; 831 + } 832 + 833 + .demo-popover-header { 834 + padding: 10px 14px; 835 + border-bottom: 1px solid var(--border); 836 + display: flex; 837 + justify-content: space-between; 838 + align-items: center; 839 + background: var(--bg-primary); 840 + border-radius: 12px 12px 0 0; 841 + font-weight: 500; 842 + font-size: 11px; 843 + color: var(--text-tertiary); 844 + text-transform: uppercase; 845 + letter-spacing: 0.5px; 846 + } 847 + 848 + .demo-popover-close { 849 + background: none; 850 + border: none; 851 + color: var(--text-tertiary); 852 + cursor: pointer; 853 + padding: 2px; 854 + font-size: 16px; 855 + line-height: 1; 856 + opacity: 0.6; 857 + transition: opacity 0.15s; 858 + } 859 + 860 + .demo-popover-close:hover { 861 + opacity: 1; 862 + } 863 + 864 + .demo-popover-scroll-area { 865 + overflow-y: auto; 866 + max-height: 340px; 867 + } 868 + 869 + .demo-comment-item { 870 + padding: 12px 14px; 871 + border-bottom: 1px solid var(--border); 872 + } 873 + 874 + .demo-comment-item:last-child { 875 + border-bottom: none; 876 + } 877 + 878 + .demo-comment-header { 879 + display: flex; 880 + align-items: center; 881 + gap: 8px; 882 + margin-bottom: 6px; 883 + } 884 + 885 + .demo-comment-avatar { 886 + width: 22px; 887 + height: 22px; 888 + border-radius: 50%; 889 + object-fit: cover; 890 + background: var(--accent); 891 + } 892 + 893 + .demo-comment-handle { 894 + font-size: 12px; 895 + font-weight: 600; 896 + color: var(--text-primary); 897 + } 898 + 899 + .demo-comment-text { 900 + font-size: 13px; 901 + line-height: 1.5; 902 + color: var(--text-primary); 903 + margin-bottom: 8px; 904 + } 905 + 906 + .demo-comment-actions { 907 + display: flex; 908 + gap: 8px; 909 + } 910 + 911 + .demo-comment-action-btn { 912 + background: none; 913 + border: none; 914 + padding: 4px 8px; 915 + color: var(--text-tertiary); 916 + font-size: 11px; 917 + cursor: pointer; 918 + border-radius: 4px; 919 + transition: all 0.15s; 920 + } 921 + 922 + .demo-comment-action-btn:hover { 923 + background: var(--bg-hover); 924 + color: var(--text-secondary); 925 + }
+1
web/src/index.css
··· 11 11 @import "./css/notifications.css"; 12 12 @import "./css/skeleton.css"; 13 13 @import "./css/utilities.css"; 14 + @import "./css/landing.css";
+738
web/src/pages/Landing.jsx
··· 1 + import { useState, useEffect, useRef } from "react"; 2 + import { Link } from "react-router-dom"; 3 + import { useAuth } from "../context/AuthContext"; 4 + import { 5 + MessageSquare, 6 + Highlighter, 7 + Users, 8 + ArrowRight, 9 + Github, 10 + Database, 11 + Shield, 12 + Zap, 13 + } from "lucide-react"; 14 + import { SiFirefox, SiGooglechrome, SiBluesky } from "react-icons/si"; 15 + import { FaEdge } from "react-icons/fa"; 16 + import logo from "../assets/logo.svg"; 17 + 18 + const isFirefox = 19 + typeof navigator !== "undefined" && /Firefox/i.test(navigator.userAgent); 20 + const isEdge = 21 + typeof navigator !== "undefined" && /Edg/i.test(navigator.userAgent); 22 + 23 + function getExtensionInfo() { 24 + if (isFirefox) { 25 + return { 26 + url: "https://addons.mozilla.org/en-US/firefox/addon/margin/", 27 + Icon: SiFirefox, 28 + label: "Firefox", 29 + }; 30 + } 31 + if (isEdge) { 32 + return { 33 + url: "https://microsoftedge.microsoft.com/addons/detail/margin/nfjnmllpdgcdnhmmggjihjbidmeadddn", 34 + Icon: FaEdge, 35 + label: "Edge", 36 + }; 37 + } 38 + return { 39 + url: "https://chromewebstore.google.com/detail/margin/cgpmbiiagnehkikhcbnhiagfomajncpa/", 40 + Icon: SiGooglechrome, 41 + label: "Chrome", 42 + }; 43 + } 44 + 45 + import { getAnnotations, normalizeAnnotation } from "../api/client"; 46 + import { formatDistanceToNow } from "date-fns"; 47 + 48 + function DemoAnnotation() { 49 + const [annotations, setAnnotations] = useState([]); 50 + const [loading, setLoading] = useState(true); 51 + const [hoverPos, setHoverPos] = useState(null); 52 + const [hoverVisible, setHoverVisible] = useState(false); 53 + const [hoverAuthors, setHoverAuthors] = useState([]); 54 + 55 + const [showPopover, setShowPopover] = useState(false); 56 + const [popoverPos, setPopoverPos] = useState(null); 57 + const [popoverAnnotations, setPopoverAnnotations] = useState([]); 58 + 59 + const highlightRef = useRef(null); 60 + const articleRef = useRef(null); 61 + 62 + useEffect(() => { 63 + getAnnotations({ source: "https://en.wikipedia.org/wiki/AT_Protocol" }) 64 + .then((res) => { 65 + const rawItems = res.items || (Array.isArray(res) ? res : []); 66 + const normalized = rawItems.map(normalizeAnnotation); 67 + setAnnotations(normalized); 68 + }) 69 + .catch((err) => { 70 + console.error("Failed to fetch demo annotations:", err); 71 + }) 72 + .finally(() => { 73 + setLoading(false); 74 + }); 75 + }, []); 76 + 77 + useEffect(() => { 78 + if (!showPopover) return; 79 + const handleClickOutside = () => setShowPopover(false); 80 + document.addEventListener("click", handleClickOutside); 81 + return () => document.removeEventListener("click", handleClickOutside); 82 + }, [showPopover]); 83 + 84 + const getMatches = () => { 85 + return annotations.filter( 86 + (a) => 87 + (a.selector?.exact && 88 + a.selector.exact.includes("A handle serves as")) || 89 + (a.quote && a.quote.includes("A handle serves as")), 90 + ); 91 + }; 92 + 93 + const handleMouseEnter = () => { 94 + const matches = getMatches(); 95 + const authorsMap = new Map(); 96 + matches.forEach((a) => { 97 + const author = a.author || a.creator || { handle: "unknown" }; 98 + const id = author.did || author.handle; 99 + if (!authorsMap.has(id)) authorsMap.set(id, author); 100 + }); 101 + const unique = Array.from(authorsMap.values()); 102 + 103 + setHoverAuthors(unique); 104 + 105 + if (highlightRef.current && articleRef.current) { 106 + const spanRect = highlightRef.current.getBoundingClientRect(); 107 + const articleRect = articleRef.current.getBoundingClientRect(); 108 + 109 + const visibleCount = Math.min(unique.length, 3); 110 + const hasOverflow = unique.length > 3; 111 + const countForCalc = visibleCount + (hasOverflow ? 1 : 0); 112 + const width = countForCalc > 0 ? countForCalc * 18 + 10 : 0; 113 + 114 + const top = spanRect.top - articleRect.top + spanRect.height / 2 - 14; 115 + const left = spanRect.left - articleRect.left - width; 116 + 117 + setHoverPos({ top, left }); 118 + setHoverVisible(true); 119 + } 120 + }; 121 + 122 + const handleMouseLeave = () => { 123 + setHoverVisible(false); 124 + }; 125 + 126 + const handleHighlightClick = (e) => { 127 + e.stopPropagation(); 128 + const matches = getMatches(); 129 + setPopoverAnnotations(matches); 130 + 131 + if (highlightRef.current && articleRef.current) { 132 + const spanRect = highlightRef.current.getBoundingClientRect(); 133 + const articleRect = articleRef.current.getBoundingClientRect(); 134 + 135 + const top = spanRect.top - articleRect.top + spanRect.height + 10; 136 + let left = spanRect.left - articleRect.left; 137 + 138 + if (left + 300 > articleRect.width) { 139 + left = articleRect.width - 300; 140 + } 141 + 142 + setPopoverPos({ top, left }); 143 + setShowPopover(true); 144 + } 145 + }; 146 + 147 + const maxShow = 3; 148 + const displayHoverAuthors = hoverAuthors.slice(0, maxShow); 149 + const hoverOverflow = hoverAuthors.length - maxShow; 150 + 151 + return ( 152 + <div className="demo-window"> 153 + <div className="demo-browser-bar"> 154 + <div className="demo-browser-dots"> 155 + <span></span> 156 + <span></span> 157 + <span></span> 158 + </div> 159 + <div className="demo-browser-url"> 160 + <span>en.wikipedia.org/wiki/AT_Protocol</span> 161 + </div> 162 + </div> 163 + <div className="demo-content"> 164 + <div 165 + className="demo-article" 166 + ref={articleRef} 167 + style={{ position: "relative" }} 168 + > 169 + {hoverPos && hoverAuthors.length > 0 && ( 170 + <div 171 + className={`demo-hover-indicator ${hoverVisible ? "visible" : ""}`} 172 + style={{ 173 + top: hoverPos.top, 174 + left: hoverPos.left, 175 + cursor: "pointer", 176 + }} 177 + onClick={handleHighlightClick} 178 + > 179 + {displayHoverAuthors.map((author, i) => 180 + author.avatar ? ( 181 + <img 182 + key={i} 183 + src={author.avatar} 184 + className="demo-hover-avatar" 185 + alt={author.handle} 186 + onError={(e) => { 187 + e.target.style.display = "none"; 188 + e.target.nextSibling.style.display = "flex"; 189 + }} 190 + /> 191 + ) : ( 192 + <div key={i} className="demo-hover-avatar-fallback"> 193 + {author.handle?.[0]?.toUpperCase() || "U"} 194 + </div> 195 + ), 196 + )} 197 + {hoverOverflow > 0 && ( 198 + <div 199 + className="demo-hover-avatar-fallback" 200 + style={{ 201 + background: "var(--bg-elevated)", 202 + color: "var(--text-secondary)", 203 + fontSize: 10, 204 + }} 205 + > 206 + +{hoverOverflow} 207 + </div> 208 + )} 209 + </div> 210 + )} 211 + 212 + {showPopover && popoverPos && ( 213 + <div 214 + className="demo-popover" 215 + style={{ 216 + top: popoverPos.top, 217 + left: popoverPos.left, 218 + }} 219 + onClick={(e) => e.stopPropagation()} 220 + > 221 + <div className="demo-popover-header"> 222 + <span> 223 + {popoverAnnotations.length}{" "} 224 + {popoverAnnotations.length === 1 ? "Comment" : "Comments"} 225 + </span> 226 + <button 227 + className="demo-popover-close" 228 + onClick={() => setShowPopover(false)} 229 + > 230 + 231 + </button> 232 + </div> 233 + <div className="demo-popover-scroll-area"> 234 + {popoverAnnotations.length === 0 ? ( 235 + <div style={{ padding: 14, fontSize: 13, color: "#666" }}> 236 + No comments 237 + </div> 238 + ) : ( 239 + popoverAnnotations.map((ann, i) => ( 240 + <div key={ann.uri || i} className="demo-comment-item"> 241 + <div className="demo-comment-header"> 242 + <img 243 + src={ann.author?.avatar || logo} 244 + className="demo-comment-avatar" 245 + onError={(e) => (e.target.src = logo)} 246 + alt="" 247 + /> 248 + <span className="demo-comment-handle"> 249 + @{ann.author?.handle || "user"} 250 + </span> 251 + </div> 252 + <div className="demo-comment-text"> 253 + {ann.text || ann.body?.value} 254 + </div> 255 + <div className="demo-comment-actions"> 256 + <button className="demo-comment-action-btn"> 257 + Reply 258 + </button> 259 + <button className="demo-comment-action-btn"> 260 + Share 261 + </button> 262 + </div> 263 + </div> 264 + )) 265 + )} 266 + </div> 267 + </div> 268 + )} 269 + <p className="demo-text"> 270 + The AT Protocol utilizes a dual identifier system: a mutable handle, 271 + in the form of a domain name, and an immutable decentralized 272 + identifier (DID). 273 + </p> 274 + <p className="demo-text"> 275 + <span 276 + className="demo-highlight" 277 + ref={highlightRef} 278 + onMouseEnter={handleMouseEnter} 279 + onMouseLeave={handleMouseLeave} 280 + onClick={handleHighlightClick} 281 + style={{ cursor: "pointer" }} 282 + > 283 + A handle serves as a verifiable user identifier. 284 + </span>{" "} 285 + Verification is by either of two equivalent methods proving control 286 + of the domain name: Either a DNS query of a resource record with the 287 + same name as the handle, or a request for a text file from a Web 288 + service with the same name. 289 + </p> 290 + <p className="demo-text"> 291 + DIDs resolve to DID documents, which contain references to key user 292 + metadata, such as the user&apos;s handle, public keys, and data 293 + repository. While any DID method could, in theory, be used by the 294 + protocol if its components provide support for the method, in 295 + practice only two methods are supported (&apos;blessed&apos;) by the 296 + protocol&apos;s reference implementations: did:plc and did:web. The 297 + validity of these identifiers can be verified by a registry which 298 + hosts the DID&apos;s associated document and a file that is hosted 299 + at a well-known location on the connected domain name, respectively. 300 + </p> 301 + </div> 302 + <div className="demo-sidebar"> 303 + <div className="demo-sidebar-header"> 304 + <div className="demo-logo-section"> 305 + <span className="demo-logo-icon"> 306 + <img src={logo} alt="" style={{ width: 16, height: 16 }} /> 307 + </span> 308 + <span className="demo-logo-text">Margin</span> 309 + </div> 310 + <div className="demo-user-section"> 311 + <span className="demo-user-handle">@margin.at</span> 312 + </div> 313 + </div> 314 + <div className="demo-page-info"> 315 + <span>en.wikipedia.org</span> 316 + </div> 317 + <div className="demo-annotations-list"> 318 + {loading ? ( 319 + <div style={{ padding: 20, textAlign: "center", color: "#666" }}> 320 + Loading... 321 + </div> 322 + ) : annotations.length > 0 ? ( 323 + annotations.map((ann, i) => ( 324 + <div 325 + key={ann.uri || i} 326 + className={`demo-annotation ${i > 0 ? "demo-annotation-secondary" : ""}`} 327 + > 328 + <div className="demo-annotation-header"> 329 + <div 330 + className="demo-avatar" 331 + style={{ background: "transparent" }} 332 + > 333 + <img 334 + src={ann.author?.avatar || logo} 335 + alt={ann.author?.handle || "User"} 336 + style={{ 337 + width: "100%", 338 + height: "100%", 339 + borderRadius: "50%", 340 + }} 341 + onError={(e) => { 342 + e.target.src = logo; 343 + }} 344 + /> 345 + </div> 346 + <div className="demo-meta"> 347 + <span className="demo-author"> 348 + @{ann.author?.handle || "margin.at"} 349 + </span> 350 + <span className="demo-time"> 351 + {ann.createdAt 352 + ? formatDistanceToNow(new Date(ann.createdAt), { 353 + addSuffix: true, 354 + }) 355 + : "recently"} 356 + </span> 357 + </div> 358 + </div> 359 + {ann.selector?.exact && ( 360 + <p className="demo-quote"> 361 + &ldquo;{ann.selector.exact}&rdquo; 362 + </p> 363 + )} 364 + <p className="demo-comment">{ann.text || ann.body?.value}</p> 365 + <button className="demo-jump-btn">Jump to text →</button> 366 + </div> 367 + )) 368 + ) : ( 369 + <div 370 + style={{ 371 + padding: 20, 372 + textAlign: "center", 373 + color: "var(--text-tertiary)", 374 + }} 375 + > 376 + No annotations found. 377 + </div> 378 + )} 379 + </div> 380 + </div> 381 + </div> 382 + </div> 383 + ); 384 + } 385 + 386 + export default function Landing() { 387 + const { user } = useAuth(); 388 + const ext = getExtensionInfo(); 389 + 390 + return ( 391 + <div className="landing-page"> 392 + <nav className="landing-nav"> 393 + <Link to="/" className="landing-logo"> 394 + <img src={logo} alt="Margin" /> 395 + <span>Margin</span> 396 + </Link> 397 + <div className="landing-nav-links"> 398 + <a 399 + href="https://github.com/margin-at/margin" 400 + target="_blank" 401 + rel="noreferrer" 402 + > 403 + GitHub 404 + </a> 405 + <a 406 + href="https://tangled.org/margin.at/margin" 407 + target="_blank" 408 + rel="noreferrer" 409 + > 410 + Tangled 411 + </a> 412 + <a 413 + href="https://bsky.app/profile/margin.at" 414 + target="_blank" 415 + rel="noreferrer" 416 + > 417 + Bluesky 418 + </a> 419 + {user ? ( 420 + <Link to="/home" className="btn btn-primary"> 421 + Open App 422 + </Link> 423 + ) : ( 424 + <Link to="/login" className="btn btn-primary"> 425 + Sign In 426 + </Link> 427 + )} 428 + </div> 429 + </nav> 430 + 431 + <section className="landing-hero"> 432 + <div className="landing-hero-content"> 433 + <div className="landing-badge"> 434 + <SiBluesky size={14} /> 435 + Built on ATProto 436 + </div> 437 + <h1 className="landing-title"> 438 + Write in the margins 439 + <br /> 440 + <span className="landing-title-accent">of the web.</span> 441 + </h1> 442 + <p className="landing-subtitle"> 443 + Margin is a social layer for reading online. Highlight passages, 444 + leave thoughts in the margins, and see what others are thinking 445 + about the pages you read. 446 + </p> 447 + <div className="landing-cta"> 448 + <a 449 + href={ext.url} 450 + target="_blank" 451 + rel="noreferrer" 452 + className="btn btn-primary btn-lg" 453 + > 454 + <ext.Icon size={18} /> 455 + Install for {ext.label} 456 + </a> 457 + {user ? ( 458 + <Link to="/home" className="btn btn-secondary btn-lg"> 459 + Open App 460 + <ArrowRight size={18} /> 461 + </Link> 462 + ) : ( 463 + <Link to="/login" className="btn btn-secondary btn-lg"> 464 + Sign In with ATProto 465 + <ArrowRight size={18} /> 466 + </Link> 467 + )} 468 + </div> 469 + <p className="landing-browsers"> 470 + Also available for{" "} 471 + {isFirefox ? ( 472 + <> 473 + <a 474 + href="https://microsoftedge.microsoft.com/addons/detail/margin/nfjnmllpdgcdnhmmggjihjbidmeadddn" 475 + target="_blank" 476 + rel="noreferrer" 477 + > 478 + Edge 479 + </a>{" "} 480 + and{" "} 481 + <a 482 + href="https://chromewebstore.google.com/detail/margin/cgpmbiiagnehkikhcbnhiagfomajncpa/" 483 + target="_blank" 484 + rel="noreferrer" 485 + > 486 + Chrome 487 + </a> 488 + </> 489 + ) : isEdge ? ( 490 + <> 491 + <a 492 + href="https://addons.mozilla.org/en-US/firefox/addon/margin/" 493 + target="_blank" 494 + rel="noreferrer" 495 + > 496 + Firefox 497 + </a>{" "} 498 + and{" "} 499 + <a 500 + href="https://chromewebstore.google.com/detail/margin/cgpmbiiagnehkikhcbnhiagfomajncpa/" 501 + target="_blank" 502 + rel="noreferrer" 503 + > 504 + Chrome 505 + </a> 506 + </> 507 + ) : ( 508 + <> 509 + <a 510 + href="https://addons.mozilla.org/en-US/firefox/addon/margin/" 511 + target="_blank" 512 + rel="noreferrer" 513 + > 514 + Firefox 515 + </a>{" "} 516 + and{" "} 517 + <a 518 + href="https://microsoftedge.microsoft.com/addons/detail/margin/nfjnmllpdgcdnhmmggjihjbidmeadddn" 519 + target="_blank" 520 + rel="noreferrer" 521 + > 522 + Edge 523 + </a> 524 + </> 525 + )} 526 + </p> 527 + </div> 528 + </section> 529 + 530 + <section className="landing-demo"> 531 + <DemoAnnotation /> 532 + </section> 533 + 534 + <section className="landing-section"> 535 + <h2 className="landing-section-title">How it works</h2> 536 + <div className="landing-steps"> 537 + <div className="landing-step"> 538 + <div className="landing-step-num">1</div> 539 + <div className="landing-step-content"> 540 + <h3>Install & Login</h3> 541 + <p> 542 + Add Margin to your browser and sign in with your AT Protocol 543 + handle. No new account needed, just your existing handle. 544 + </p> 545 + </div> 546 + </div> 547 + <div className="landing-step"> 548 + <div className="landing-step-num">2</div> 549 + <div className="landing-step-content"> 550 + <h3>Annotate the Web</h3> 551 + <p> 552 + Highlight text on any page. Leave notes in the margins, ask 553 + questions, or add context to the conversation precisely where it 554 + belongs. 555 + </p> 556 + </div> 557 + </div> 558 + <div className="landing-step"> 559 + <div className="landing-step-num">3</div> 560 + <div className="landing-step-content"> 561 + <h3>Share & Discover</h3> 562 + <p> 563 + Your annotations are published to your PDS. Discover what the 564 + community is reading and discussing across the web. 565 + </p> 566 + </div> 567 + </div> 568 + </div> 569 + </section> 570 + 571 + <section className="landing-section landing-section-alt"> 572 + <div className="landing-features-grid"> 573 + <div className="landing-feature"> 574 + <div className="landing-feature-icon"> 575 + <Highlighter size={20} /> 576 + </div> 577 + <h3>Universal Highlights</h3> 578 + <p> 579 + Save passages from any article, paper, or post. Your collection 580 + travels with you, independent of any single platform. 581 + </p> 582 + </div> 583 + <div className="landing-feature"> 584 + <div className="landing-feature-icon"> 585 + <MessageSquare size={20} /> 586 + </div> 587 + <h3>Universal Notes</h3> 588 + <p> 589 + Move the discussion out of the comments section. Contextual 590 + conversations that live right alongside the content. 591 + </p> 592 + </div> 593 + <div className="landing-feature"> 594 + <div className="landing-feature-icon"> 595 + <Shield size={20} /> 596 + </div> 597 + <h3>Open Identity</h3> 598 + <p> 599 + Your data, your handle, your graph. Built on the AT Protocol for 600 + true ownership and portability. 601 + </p> 602 + </div> 603 + <div className="landing-feature"> 604 + <div className="landing-feature-icon"> 605 + <Users size={20} /> 606 + </div> 607 + <h3>Community Context</h3> 608 + <p> 609 + See the web with fresh eyes. Discover highlights and notes from 610 + other readers directly on the page. 611 + </p> 612 + </div> 613 + </div> 614 + </section> 615 + 616 + <section className="landing-section landing-protocol"> 617 + <div className="landing-protocol-grid"> 618 + <div className="landing-protocol-main"> 619 + <h2>Your data, your identity</h2> 620 + <p> 621 + Margin is built on the{" "} 622 + <a href="https://atproto.com" target="_blank" rel="noreferrer"> 623 + AT Protocol 624 + </a> 625 + , the same open protocol that powers Bluesky. Sign in with your 626 + existing Bluesky account or create a new one in your preferred 627 + PDS. 628 + </p> 629 + <p> 630 + Your annotations are stored in your PDS. You can export them 631 + anytime, use them with other apps, or self-host your own server. 632 + No vendor lock-in. 633 + </p> 634 + </div> 635 + <div className="landing-protocol-features"> 636 + <div className="landing-protocol-item"> 637 + <Database size={20} /> 638 + <div> 639 + <strong>Portable data</strong> 640 + <span>Export or migrate anytime</span> 641 + </div> 642 + </div> 643 + <div className="landing-protocol-item"> 644 + <Shield size={20} /> 645 + <div> 646 + <strong>You own your identity</strong> 647 + <span>Use your own domain as handle</span> 648 + </div> 649 + </div> 650 + <div className="landing-protocol-item"> 651 + <Zap size={20} /> 652 + <div> 653 + <strong>Interoperable</strong> 654 + <span>Works with the ATProto ecosystem</span> 655 + </div> 656 + </div> 657 + <div className="landing-protocol-item"> 658 + <Github size={20} /> 659 + <div> 660 + <strong>Open source</strong> 661 + <span>Audit, contribute, self-host</span> 662 + </div> 663 + </div> 664 + </div> 665 + </div> 666 + </section> 667 + 668 + <section className="landing-section landing-final-cta"> 669 + <h2>Start annotating today</h2> 670 + <p>Free and open source. Sign in with ATProto to get started.</p> 671 + <div className="landing-cta"> 672 + <a 673 + href={ext.url} 674 + target="_blank" 675 + rel="noreferrer" 676 + className="btn btn-primary btn-lg" 677 + > 678 + <ext.Icon size={18} /> 679 + Get the Extension 680 + </a> 681 + </div> 682 + </section> 683 + 684 + <footer className="landing-footer"> 685 + <div className="landing-footer-grid"> 686 + <div className="landing-footer-brand"> 687 + <Link to="/" className="landing-logo"> 688 + <img src={logo} alt="Margin" /> 689 + <span>Margin</span> 690 + </Link> 691 + <p>Write in the margins of the web.</p> 692 + </div> 693 + <div className="landing-footer-links"> 694 + <div className="landing-footer-col"> 695 + <h4>Product</h4> 696 + <a href={ext.url} target="_blank" rel="noreferrer"> 697 + Browser Extension 698 + </a> 699 + <Link to="/home">Web App</Link> 700 + </div> 701 + <div className="landing-footer-col"> 702 + <h4>Community</h4> 703 + <a 704 + href="https://github.com/margin-at/margin" 705 + target="_blank" 706 + rel="noreferrer" 707 + > 708 + GitHub 709 + </a> 710 + <a 711 + href="https://tangled.org/margin.at/margin" 712 + target="_blank" 713 + rel="noreferrer" 714 + > 715 + Tangled 716 + </a> 717 + <a 718 + href="https://bsky.app/profile/margin.at" 719 + target="_blank" 720 + rel="noreferrer" 721 + > 722 + Bluesky 723 + </a> 724 + </div> 725 + <div className="landing-footer-col"> 726 + <h4>Legal</h4> 727 + <Link to="/privacy">Privacy Policy</Link> 728 + <Link to="/terms">Terms of Service</Link> 729 + </div> 730 + </div> 731 + </div> 732 + <div className="landing-footer-bottom"> 733 + <p>© {new Date().getFullYear()} Margin. Open source under MIT.</p> 734 + </div> 735 + </footer> 736 + </div> 737 + ); 738 + }