| … | … | @@ -481,10 +481,10 @@ export function countThread (replies) { |
| 481 | 481 | |
| 482 | 482 | // ── page shell ──────────────────────────────────────────────── |
| 483 | 483 | const STYLE = ` |
| 484 | | body{font-family:system-ui,sans-serif;max-width:620px;margin:2rem auto;padding:0 1rem;color:#111} |
| 485 | | a{color:#1a56db}h1{font-size:1.4rem}h1 small{color:#888;font-weight:400} |
| 484 | body{font-family:system-ui,sans-serif;max-width:620px;margin:2rem auto;padding:0 1rem;color:var(--fg)} |
| 485 | a{color:var(--accent)}h1{font-size:1.4rem}h1 small{color:var(--fg-faint);font-weight:400} |
| 486 | 486 | nav{margin:.4rem 0 1rem;display:flex;gap:1rem;font-size:.9rem} |
| 487 | | .post{position:relative;border-bottom:1px solid #e5e5e5;padding:.8rem 0}.post .text{white-space:pre-wrap} |
| 487 | .post{position:relative;border-bottom:1px solid var(--line);padding:.8rem 0}.post .text{white-space:pre-wrap} |
| 488 | 488 | /* The whole-entry link emitted by renderPost. It covers the card and sits UNDER everything, so the |
| 489 | 489 | card is one big click target for "open this thread". */ |
| 490 | 490 | .pstretch{position:absolute;inset:0;z-index:0} |
| … | … | @@ -499,19 +499,19 @@ nav{margin:.4rem 0 1rem;display:flex;gap:1rem;font-size:.9rem} |
| 499 | 499 | .post .byline,.post .inreply a,.post .text a,.post .meta a,.post .meta form,.post .media a,.post .quote,.post .card,.post .delw,details.post>summary{position:relative;z-index:1} |
| 500 | 500 | /* A click target with no hover state is invisible until you happen to click it. Kept faint: it must |
| 501 | 501 | read as "this row is live", not as a selected state. */ |
| 502 | | .post:hover{background:#fafafa} |
| 502 | .post:hover{background:var(--bg-soft)} |
| 503 | 503 | /* No hover where there is no link to follow: the focused post of a thread, and a collapsed post while |
| 504 | 504 | it is still closed (its overlay lives inside the hidden content, so there is nothing to click yet). */ |
| 505 | 505 | .focus .post:hover,details.post.collapsed:not([open]):hover{background:none} |
| 506 | | .post .media{margin-top:.5rem}.post .media img{max-width:100%;height:auto;border-radius:.5rem;display:block;margin:.3rem 0;background:#f3f4f6} |
| 507 | | details.post>summary{cursor:pointer;color:#888;font-style:italic;list-style:revert} |
| 508 | | .meta{color:#888;font-size:.8rem;margin-top:.35rem}.meta .did{color:#888} |
| 506 | .post .media{margin-top:.5rem}.post .media img{max-width:100%;height:auto;border-radius:.5rem;display:block;margin:.3rem 0;background:var(--bg-soft)} |
| 507 | details.post>summary{cursor:pointer;color:var(--fg-faint);font-style:italic;list-style:revert} |
| 508 | .meta{color:var(--fg-faint);font-size:.8rem;margin-top:.35rem}.meta .did{color:var(--fg-faint)} |
| 509 | 509 | .controls{display:flex;gap:.5rem;align-items:center;margin-top:.4rem}.controls select,.controls button{font:inherit;padding:.3rem} |
| 510 | | .badge{display:inline-block;font-size:.7rem;font-weight:600;padding:.05rem .4rem;border-radius:.6rem;margin-left:.4rem;background:#e8f0fe;color:#1a56db} |
| 510 | .badge{display:inline-block;font-size:.7rem;font-weight:600;padding:.05rem .4rem;border-radius:.6rem;margin-left:.4rem;background:#e8f0fe;color:var(--accent)} |
| 511 | 511 | .lbl{display:inline-block;font-size:.7rem;font-weight:600;padding:.05rem .4rem;border-radius:.6rem;margin-left:.4rem;background:#fde8e8;color:#b91c1c} |
| 512 | 512 | .tag{text-decoration:none}.tag:hover{text-decoration:underline} |
| 513 | | .viewhdr{margin:.6rem 0;font-weight:600}.empty{color:#888;font-style:italic} |
| 514 | | .who{font-size:.85rem;color:#888;margin-left:auto} |
| 513 | .viewhdr{margin:.6rem 0;font-weight:600}.empty{color:var(--fg-faint);font-style:italic} |
| 514 | .who{font-size:.85rem;color:var(--fg-faint);margin-left:auto} |
| 515 | 515 | .phead{display:flex;align-items:center;gap:.5rem;flex-wrap:wrap;margin-bottom:.35rem} |
| 516 | 516 | .byline{display:flex;align-items:center;gap:.5rem;text-decoration:none;color:inherit;min-width:0} |
| 517 | 517 | .byline:hover .name{text-decoration:underline} |
| … | … | @@ -519,28 +519,28 @@ details.post>summary{cursor:pointer;color:#888;font-style:italic;list-style:reve |
| 519 | 519 | .pfp{width:36px;height:36px;border-radius:50%;object-fit:cover;background:#e5e7eb;flex:none} |
| 520 | 520 | .pfp.none{display:inline-block;background:#e5e7eb} |
| 521 | 521 | .pfp.big{width:72px;height:72px} |
| 522 | | .meta .permalink{color:#888;text-decoration:none}.meta .permalink:hover{text-decoration:underline} |
| 522 | .meta .permalink{color:var(--fg-faint);text-decoration:none}.meta .permalink:hover{text-decoration:underline} |
| 523 | 523 | .mention{text-decoration:none;font-weight:500}.mention:hover{text-decoration:underline} |
| 524 | 524 | .profhdr{display:flex;gap:.9rem;align-items:flex-start;margin:.6rem 0 1rem} |
| 525 | 525 | .profname{font-size:1.15rem;font-weight:700} |
| 526 | | .profdid code{font-size:.75rem;color:#888;word-break:break-all} |
| 526 | .profdid code{font-size:.75rem;color:var(--fg-faint);word-break:break-all} |
| 527 | 527 | .profbio{margin:.4rem 0 0;white-space:pre-wrap} |
| 528 | 528 | .aturi code{font-size:.75rem;word-break:break-all} |
| 529 | | .replyc{color:#888;text-decoration:none}.replyc:hover{text-decoration:underline} |
| 529 | .replyc{color:var(--fg-faint);text-decoration:none}.replyc:hover{text-decoration:underline} |
| 530 | 530 | .rpf{display:inline;margin-left:.15rem} |
| 531 | | .rpbtn{font:inherit;font-size:.9rem;line-height:1;border:none;background:none;color:#888;cursor:pointer;padding:0 .15rem} |
| 532 | | .rpbtn.on{color:#17bf63}.rpbtn:hover{color:#17bf63} |
| 533 | | .rps{color:#888;font-size:.8rem} |
| 534 | | .inreply{font-size:.8rem;color:#888;margin-bottom:.25rem} |
| 535 | | .replyf{margin:.8rem 0;padding:.6rem;border:1px solid #e5e5e5;border-radius:.5rem} |
| 536 | | .replyf textarea{width:100%;box-sizing:border-box;font:inherit;padding:.4rem;border:1px solid #ccc;border-radius:.3rem} |
| 531 | .rpbtn{font:inherit;font-size:.9rem;line-height:1;border:none;background:none;color:var(--fg-faint);cursor:pointer;padding:0 .15rem} |
| 532 | .rpbtn.on{color:var(--repost)}.rpbtn:hover{color:var(--repost)} |
| 533 | .rps{color:var(--fg-faint);font-size:.8rem} |
| 534 | .inreply{font-size:.8rem;color:var(--fg-faint);margin-bottom:.25rem} |
| 535 | .replyf{margin:.8rem 0;padding:.6rem;border:1px solid var(--line);border-radius:.5rem} |
| 536 | .replyf textarea{width:100%;box-sizing:border-box;font:inherit;padding:.4rem;border:1px solid var(--line);border-radius:.3rem} |
| 537 | 537 | .replyf p{display:flex;align-items:center;gap:.5rem;margin:.4rem 0 0} |
| 538 | 538 | .replyf button{font:inherit;padding:.3rem .9rem;cursor:pointer} |
| 539 | | .thread{margin-top:1rem;border-top:2px solid #e5e5e5;padding-top:.5rem} |
| 540 | | .thread h3{font-size:.9rem;color:#666;margin:.3rem 0} |
| 539 | .thread{margin-top:1rem;border-top:2px solid var(--line);padding-top:.5rem} |
| 540 | .thread h3{font-size:.9rem;color:var(--fg-dim);margin:.3rem 0} |
| 541 | 541 | /* Nested replies: children live INSIDE the parent node, so this one rule gives the accumulating indent |
| 542 | 542 | and the thread line at every depth. Capped padding keeps a deep chain from marching off-screen. */ |
| 543 | | .tnode .tnode{margin-left:.5rem;padding-left:.7rem;border-left:2px solid #eee} |
| 543 | .tnode .tnode{margin-left:.5rem;padding-left:.7rem;border-left:2px solid var(--line-soft)} |
| 544 | 544 | .focus{border-left:3px solid #444;padding-left:.6rem;margin:.4rem 0} |
| 545 | 545 | /* A lens is a NAMED FOREIGN source. Styled visibly apart from our own posts — a reader must never have |
| 546 | 546 | to guess whether a reply is one this node holds or one it is relaying from someone else's AppView. */ |
| … | … | @@ -548,17 +548,17 @@ details.post>summary{cursor:pointer;color:#888;font-style:italic;list-style:reve |
| 548 | 548 | /* The ancestor fill sits ABOVE the focused post, so its rule belongs on the bottom edge — otherwise the |
| 549 | 549 | section reads as separating itself from the post it precedes. */ |
| 550 | 550 | .lensup{margin:.3rem 0 .6rem;border-top:0;border-bottom:1px dashed #cbd5e1;padding:0 0 .4rem} |
| 551 | | .lens h3{font-size:.85rem;color:#666;font-weight:600;margin:.3rem 0} |
| 551 | .lens h3{font-size:.85rem;color:var(--fg-dim);font-weight:600;margin:.3rem 0} |
| 552 | 552 | .lensname{color:#3b5bdb} |
| 553 | 553 | .lpost{border-left:2px solid #dbe4ff;padding:.3rem 0 .3rem .7rem;margin:.4rem 0} |
| 554 | 554 | .lwho{font-size:.9rem;font-weight:600} |
| 555 | | .lhandle{font-weight:400;color:#888} |
| 555 | .lhandle{font-weight:400;color:var(--fg-faint)} |
| 556 | 556 | .ltext{white-space:pre-wrap;margin:.15rem 0} |
| 557 | 557 | .lensoff{font-size:.78rem;margin-top:1rem} |
| 558 | | .lensnote{font-size:.8rem;color:#666;margin:.4rem 0 0} |
| 559 | | .qbtn{color:#888;text-decoration:none}.qbtn:hover{color:#1a56db} |
| 558 | .lensnote{font-size:.8rem;color:var(--fg-dim);margin:.4rem 0 0} |
| 559 | .qbtn{color:var(--fg-faint);text-decoration:none}.qbtn:hover{color:var(--accent)} |
| 560 | 560 | .delw{display:inline} |
| 561 | | .delw>summary{display:inline;list-style:none;cursor:pointer;color:#888} |
| 561 | .delw>summary{display:inline;list-style:none;cursor:pointer;color:var(--fg-faint)} |
| 562 | 562 | .delw>summary::-webkit-details-marker{display:none} |
| 563 | 563 | .delw>summary:hover{color:#c92a2a} |
| 564 | 564 | .delf{display:inline;margin-left:.3rem} |
| … | … | @@ -579,7 +579,7 @@ details.post>summary{cursor:pointer;color:#888;font-style:italic;list-style:reve |
| 579 | 579 | .navsearch input:focus{outline:2px solid var(--accent,#1a56db);outline-offset:1px;width:11rem} |
| 580 | 580 | .searchbox{display:flex;gap:.5rem;margin:.6rem 0 1rem} |
| 581 | 581 | .searchbox input{flex:1;font:inherit;padding:.45rem .7rem;border:1px solid var(--line,#e5e5e5);border-radius:999px;background:var(--bg,#fff);color:inherit} |
| 582 | | .searchbox button{font:inherit;padding:.4rem 1rem;border:none;border-radius:999px;background:var(--accent,#1a56db);color:#fff;font-weight:600;cursor:pointer} |
| 582 | .searchbox button{font:inherit;padding:.4rem 1rem;border:none;border-radius:999px;background:var(--accent,#1a56db);color:var(--bg);font-weight:600;cursor:pointer} |
| 583 | 583 | .actorrow{padding:.4rem 0;border-bottom:1px solid var(--line-soft,#f0f1f3)} |
| 584 | 584 | .gatenote{font-size:.85rem;color:var(--fg-dim,#666);margin:.6rem 0 0} |
| 585 | 585 | .gateoff{font-size:.85rem;margin:.6rem 0 0} |
| … | … | @@ -629,13 +629,13 @@ details.post>summary{cursor:pointer;color:#888;font-style:italic;list-style:reve |
| 629 | 629 | .profbanner img{display:block;width:100%;aspect-ratio:3/1;object-fit:cover} |
| 630 | 630 | .profbanner.small img{aspect-ratio:4/1} |
| 631 | 631 | .bmf{display:inline} |
| 632 | | .bmbtn{font:inherit;font-size:.9rem;line-height:1;border:none;background:none;color:#888;cursor:pointer;padding:.3rem .45rem;margin:-.3rem -.1rem;transition:transform .08s ease,color .12s ease;touch-action:manipulation} |
| 632 | .bmbtn{font:inherit;font-size:.9rem;line-height:1;border:none;background:none;color:var(--fg-faint);cursor:pointer;padding:.3rem .45rem;margin:-.3rem -.1rem;transition:transform .08s ease,color .12s ease;touch-action:manipulation} |
| 633 | 633 | .bmbtn:hover{color:#f59f00}.bmbtn.on{color:#f59f00} |
| 634 | 634 | .bmbtn:active{transform:scale(.88)} |
| 635 | | .bmbtn:focus-visible{outline:2px solid #1a56db;outline-offset:2px;border-radius:.3rem} |
| 635 | .bmbtn:focus-visible{outline:2px solid var(--accent);outline-offset:2px;border-radius:.3rem} |
| 636 | 636 | /* a11y. A skip link is the difference between reaching the content and tabbing the whole nav on every |
| 637 | 637 | page; it is visually hidden until focused, which is the one time it matters. */ |
| 638 | | .skip{position:absolute;left:-9999px;top:0;z-index:200;background:var(--accent,#1a56db);color:#fff;padding:.5rem .9rem;border-radius:0 0 .4rem 0} |
| 638 | .skip{position:absolute;left:-9999px;top:0;z-index:200;background:var(--accent,#1a56db);color:var(--bg);padding:.5rem .9rem;border-radius:0 0 .4rem 0} |
| 639 | 639 | .skip:focus{left:0} |
| 640 | 640 | main:focus{outline:none} |
| 641 | 641 | /* Visually hidden but ANNOUNCED — for labels that would be redundant on screen and essential aloud. |
| … | … | @@ -661,27 +661,27 @@ a:focus-visible,button:focus-visible,input:focus-visible,textarea:focus-visible, |
| 661 | 661 | .qpreview{font-size:.85rem;color:var(--fg-dim,#666);border-left:3px solid var(--accent,#1a56db);padding:.2rem .5rem;margin-bottom:.5rem} |
| 662 | 662 | .qptext{font-style:italic} |
| 663 | 663 | .qcancel{font-size:.8rem} |
| 664 | | .stath{font-size:.85rem;color:#666;margin:1rem 0 .3rem;text-transform:uppercase;letter-spacing:.03em} |
| 665 | | .statblk{border:1px solid #e5e5e5;border-radius:4px;padding:.5rem .6rem;margin:.4rem 0} |
| 664 | .stath{font-size:.85rem;color:var(--fg-dim);margin:1rem 0 .3rem;text-transform:uppercase;letter-spacing:.03em} |
| 665 | .statblk{border:1px solid var(--line);border-radius:4px;padding:.5rem .6rem;margin:.4rem 0} |
| 666 | 666 | .statblk.alarm{border-color:#e03131;background:#fff5f5} |
| 667 | | .statdid{font-size:.8rem;color:#666} |
| 667 | .statdid{font-size:.8rem;color:var(--fg-dim)} |
| 668 | 668 | .statsum{font-size:.9rem;margin:.2rem 0 .4rem} |
| 669 | 669 | .stattbl{border-collapse:collapse;font-size:.8rem;width:100%} |
| 670 | | .stattbl th{text-align:left;color:#888;font-weight:600;padding:.15rem .4rem .15rem 0} |
| 670 | .stattbl th{text-align:left;color:var(--fg-faint);font-weight:600;padding:.15rem .4rem .15rem 0} |
| 671 | 671 | .stattbl td{padding:.15rem .4rem .15rem 0;border-top:1px solid #f1f1f1} |
| 672 | 672 | .stattbl tr.chosen td{font-weight:600;background:#f8f9ff} |
| 673 | 673 | .alarmnote{color:#c92a2a;font-size:.82rem;margin:.4rem 0 0} |
| 674 | 674 | .ok{color:#2b8a3e}.bad{color:#c92a2a} |
| 675 | | .ptabs{display:flex;gap:.15rem;border-bottom:1px solid #e5e5e5;margin:.6rem 0 .3rem;flex-wrap:wrap} |
| 676 | | .ptab{padding:.35rem .7rem;font-size:.88rem;color:#666;text-decoration:none;border-bottom:2px solid transparent} |
| 677 | | .ptab:hover{color:#111} |
| 678 | | .ptab.on{color:#111;font-weight:600;border-bottom-color:#444} |
| 675 | .ptabs{display:flex;gap:.15rem;border-bottom:1px solid var(--line);margin:.6rem 0 .3rem;flex-wrap:wrap} |
| 676 | .ptab{padding:.35rem .7rem;font-size:.88rem;color:var(--fg-dim);text-decoration:none;border-bottom:2px solid transparent} |
| 677 | .ptab:hover{color:var(--fg)} |
| 678 | .ptab.on{color:var(--fg);font-weight:600;border-bottom-color:#444} |
| 679 | 679 | .ancestor{opacity:.72} |
| 680 | | .missing{font-size:.8rem;border-left:2px dashed #ccc;padding-left:.6rem} |
| 681 | | .likes{color:#888;font-size:.8rem} |
| 680 | .missing{font-size:.8rem;border-left:2px dashed var(--line);padding-left:.6rem} |
| 681 | .likes{color:var(--fg-faint);font-size:.8rem} |
| 682 | 682 | .likef{display:inline;margin-left:.5rem} |
| 683 | | .likebtn{font:inherit;font-size:.9rem;line-height:1;border:none;background:none;color:#888;cursor:pointer;padding:0 .15rem} |
| 684 | | .likebtn.on{color:#e0245e}.likebtn:hover{color:#e0245e} |
| 683 | .likebtn{font:inherit;font-size:.9rem;line-height:1;border:none;background:none;color:var(--fg-faint);cursor:pointer;padding:0 .15rem} |
| 684 | .likebtn.on{color:var(--like)}.likebtn:hover{color:var(--like)} |
| 685 | 685 | /* Responsive controls. These used to be plain form POSTs that reloaded the whole page to toggle a heart — |
| 686 | 686 | correct, and it felt broken. /static/actions.js now posts JSON and updates in place; this is the part |
| 687 | 687 | that makes the press FEEL like it landed: |
| … | … | @@ -696,25 +696,25 @@ a:focus-visible,button:focus-visible,input:focus-visible,textarea:focus-visible, |
| 696 | 696 | .likebtn.failed,.rpbtn.failed,.followbtn.failed{color:#c92a2a;animation:shake .25s} |
| 697 | 697 | @keyframes shake{25%{transform:translateX(-2px)}75%{transform:translateX(2px)}} |
| 698 | 698 | /* Keyboard users get the same affordance the mouse does — these are real buttons, so make focus visible. */ |
| 699 | | .likebtn:focus-visible,.rpbtn:focus-visible,.followbtn:focus-visible,.replyc:focus-visible{outline:2px solid #1a56db;outline-offset:2px;border-radius:.3rem} |
| 699 | .likebtn:focus-visible,.rpbtn:focus-visible,.followbtn:focus-visible,.replyc:focus-visible{outline:2px solid var(--accent);outline-offset:2px;border-radius:.3rem} |
| 700 | 700 | @media(prefers-reduced-motion:reduce){.likebtn,.rpbtn,.followbtn{transition:none}.likebtn.failed,.rpbtn.failed,.followbtn.failed{animation:none}} |
| 701 | 701 | .followf{margin:.5rem 0 0} |
| 702 | | .followbtn{font:inherit;border:1px solid #1a56db;background:#1a56db;color:#fff;border-radius:.4rem;padding:.3rem .9rem;cursor:pointer} |
| 703 | | .followbtn.on{background:none;color:#1a56db} |
| 704 | | .scopenote{color:#888;font-weight:400} |
| 702 | .followbtn{font:inherit;border:1px solid var(--accent);background:var(--accent);color:var(--bg);border-radius:.4rem;padding:.3rem .9rem;cursor:pointer} |
| 703 | .followbtn.on{background:none;color:var(--accent)} |
| 704 | .scopenote{color:var(--fg-faint);font-weight:400} |
| 705 | 705 | .profnet{margin:.3rem 0;font-size:.92rem} |
| 706 | 706 | .fol{display:inline-block;margin:.1rem .3rem .1rem 0;color:#356} |
| 707 | | .folmore{color:#888} |
| 708 | | .profgraph{margin:.4rem 0 0;font-size:.9rem;color:#666} |
| 709 | | .cc{font-size:.8rem;color:#888;margin-left:auto}.cc.over{color:#b91c1c;font-weight:600} |
| 710 | | .proform label{font-size:.85rem;color:#666} |
| 711 | | .proform input,.proform textarea{width:100%;box-sizing:border-box;font:inherit;padding:.4rem;border:1px solid #ccc;border-radius:.3rem} |
| 707 | .folmore{color:var(--fg-faint)} |
| 708 | .profgraph{margin:.4rem 0 0;font-size:.9rem;color:var(--fg-dim)} |
| 709 | .cc{font-size:.8rem;color:var(--fg-faint);margin-left:auto}.cc.over{color:#b91c1c;font-weight:600} |
| 710 | .proform label{font-size:.85rem;color:var(--fg-dim)} |
| 711 | .proform input,.proform textarea{width:100%;box-sizing:border-box;font:inherit;padding:.4rem;border:1px solid var(--line);border-radius:.3rem} |
| 712 | 712 | .proform button{font:inherit;padding:.35rem .9rem;cursor:pointer} |
| 713 | 713 | .proform .cc{margin-left:0;display:inline-block;margin-top:.15rem} |
| 714 | | .imgpick{display:inline-block;cursor:pointer;border:1px solid #ccc;border-radius:.3rem;padding:.25rem .6rem;font-size:.85rem} |
| 714 | .imgpick{display:inline-block;cursor:pointer;border:1px solid var(--line);border-radius:.3rem;padding:.25rem .6rem;font-size:.85rem} |
| 715 | 715 | .imgprev{display:flex;gap:.5rem;flex-wrap:wrap;margin-top:.5rem} |
| 716 | 716 | .imgprev .thumb{display:flex;flex-direction:column;gap:.2rem;width:140px} |
| 717 | | .imgprev .thumb img{width:140px;height:100px;object-fit:cover;border-radius:.3rem;background:#f3f4f6} |
| 717 | .imgprev .thumb img{width:140px;height:100px;object-fit:cover;border-radius:.3rem;background:var(--bg-soft)} |
| 718 | 718 | .imgprev .thumb input,.imgprev .thumb button{font:inherit;font-size:.75rem;padding:.2rem;width:100%;box-sizing:border-box} |
| 719 | 719 | .imgprev .thumb button{cursor:pointer} |
| 720 | 720 | @media(prefers-color-scheme:dark){body{background:#111;color:#eee}a,.tag{color:#9ec5ff}.post{border-color:#333} |