(READ ONLY) Margin is an open annotation layer for the internet. Powered by the AT Protocol. margin.at
extension web atproto comments
98
fork

Configure Feed

Select the types of activity you want to include in your feed.

Landing Page

scanash00 4f133c90 1257864d

+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 + }