OverviewFilesCommitsIssuesDocsReleasesPeers

favela-web commits / a807f99

render: semantic tokens, and the base layer's dark patch is now dead code
Adds the tokens the migration had nothing to map to — danger/warn/ok/bookmark, --badge-bg, and the lens family — then migrates the remaining 27 literals onto them and deletes the hand-written dark patch, which existed ONLY because those rules were written as literals. Two known-failing values were corrected on the way in rather than tokenised as-is, because shipping a token with a measured failure is how --fg-faint regressed in the first place: --ok #2b8a3e 4.37:1 -> #2a833b 4.76:1 (text, was under AA) --bookmark #f59f00 2.13:1 -> #c77800 3.43:1 (icon, was under even 3:1) --warn stays #b8860b: it is only ever a border on .stale, where 3:1 applies and 3.25:1 clears it. Dark values are measured too — danger-text 10.95:1, ok 10.84:1, bookmark 8.85:1, lens 9.47:1 on #111. The patch could NOT simply be deleted. Four things it painted had no token-based rule underneath, and would have broken: - body had NO background at all (computed rgba(0,0,0,0)); the patch was the only thing painting the page dark. Now background:var(--bg), which also stops the page borrowing whatever is behind it. - .controls button was never themed, only .controls select was — a button kept the UA's light chrome in dark mode. - .pfp/.pfp.none placeholder was a raw #e5e7eb -> var(--line). - .cc turned out to be fine already. Verified: dark body is rgb(17,17,17) with all elements resolving through tokens; light diff is 51 properties, all explained (the mobile body 18->16 from the previous commit, line-heights following it, the new explicit body background, .pfp onto --line, one radius). No overflow at 360/390/768/1280. Five prefers-color-scheme blocks are now three; the two left style other pages' own <style> blocks and are a separate concern. Phase 2 of the visual-parity order. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
a807f99ac64dca565761d07f4966cfd03bc1975b
by plankatron <fleeky@gmail.com> · 2026-08-24T14:18:38.000Z

Changed files (2) +47 -31

lib/render.js +46 -30
……@@ -507,8 +507,8 @@ nav{margin:.4rem 0 1rem;display:flex;gap:1rem;font-size:.9rem}
507507details.post>summary{cursor:pointer;color:var(--fg-faint);font-style:italic;list-style:revert}
508508.meta{color:var(--fg-faint);font-size:.8rem;margin-top:.35rem}.meta .did{color:var(--fg-faint)}
509509.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:var(--accent)}
511.lbl{display:inline-block;font-size:.7rem;font-weight:600;padding:.05rem .4rem;border-radius:.6rem;margin-left:.4rem;background:#fde8e8;color:#b91c1c}
510.badge{display:inline-block;font-size:.7rem;font-weight:600;padding:.05rem .4rem;border-radius:.6rem;margin-left:.4rem;background:var(--badge-bg);color:var(--accent)}
511.lbl{display:inline-block;font-size:.7rem;font-weight:600;padding:.05rem .4rem;border-radius:.6rem;margin-left:.4rem;background:var(--danger-bg);color:var(--danger-text)}
512512.tag{text-decoration:none}.tag:hover{text-decoration:underline}
513513.viewhdr{margin:.6rem 0;font-weight:600}.empty{color:var(--fg-faint);font-style:italic}
514514.who{font-size:.85rem;color:var(--fg-faint);margin-left:auto}
……@@ -516,8 +516,8 @@ details.post>summary{cursor:pointer;color:var(--fg-faint);font-style:italic;list
516516.byline{display:flex;align-items:center;gap:.5rem;text-decoration:none;color:inherit;min-width:0}
517517.byline:hover .name{text-decoration:underline}
518518.byline .name{font-weight:600;font-size:.95rem;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
519.pfp{width:36px;height:36px;border-radius:50%;object-fit:cover;background:#e5e7eb;flex:none}
520.pfp.none{display:inline-block;background:#e5e7eb}
519.pfp{width:36px;height:36px;border-radius:50%;object-fit:cover;background:var(--line);flex:none}
520.pfp.none{display:inline-block;background:var(--line)}
521521.pfp.big{width:72px;height:72px}
522522.meta .permalink{color:var(--fg-faint);text-decoration:none}.meta .permalink:hover{text-decoration:underline}
523523.mention{text-decoration:none;font-weight:500}.mention:hover{text-decoration:underline}
……@@ -544,13 +544,13 @@ details.post>summary{cursor:pointer;color:var(--fg-faint);font-style:italic;list
544544.focus{border-left:3px solid #444;padding-left:.6rem;margin:.4rem 0}
545545/* A lens is a NAMED FOREIGN source. Styled visibly apart from our own posts — a reader must never have
546546 to guess whether a reply is one this node holds or one it is relaying from someone else's AppView. */
547.lens{margin-top:1rem;border-top:1px dashed #cbd5e1;padding-top:.5rem}
547.lens{margin-top:1rem;border-top:1px dashed var(--lens-line);padding-top:.5rem}
548548/* The ancestor fill sits ABOVE the focused post, so its rule belongs on the bottom edge — otherwise the
549549 section reads as separating itself from the post it precedes. */
550.lensup{margin:.3rem 0 .6rem;border-top:0;border-bottom:1px dashed #cbd5e1;padding:0 0 .4rem}
550.lensup{margin:.3rem 0 .6rem;border-top:0;border-bottom:1px dashed var(--lens-line);padding:0 0 .4rem}
551551.lens h3{font-size:.85rem;color:var(--fg-dim);font-weight:600;margin:.3rem 0}
552.lensname{color:#3b5bdb}
553.lpost{border-left:2px solid #dbe4ff;padding:.3rem 0 .3rem .7rem;margin:.4rem 0}
552.lensname{color:var(--lens)}
553.lpost{border-left:2px solid var(--lens-edge);padding:.3rem 0 .3rem .7rem;margin:.4rem 0}
554554.lwho{font-size:.9rem;font-weight:600}
555555.lhandle{font-weight:400;color:var(--fg-faint)}
556556.ltext{white-space:pre-wrap;margin:.15rem 0}
……@@ -560,9 +560,9 @@ details.post>summary{cursor:pointer;color:var(--fg-faint);font-style:italic;list
560560.delw{display:inline}
561561.delw>summary{display:inline;list-style:none;cursor:pointer;color:var(--fg-faint)}
562562.delw>summary::-webkit-details-marker{display:none}
563.delw>summary:hover{color:#c92a2a}
563.delw>summary:hover{color:var(--danger)}
564564.delf{display:inline;margin-left:.3rem}
565.delbtn{font:inherit;font-size:.78rem;border:1px solid #c92a2a;background:#c92a2a;color:#fff;border-radius:999px;padding:.15rem .5rem;cursor:pointer}
565.delbtn{font:inherit;font-size:.78rem;border:1px solid var(--danger);background:var(--danger);color:#fff;border-radius:999px;padding:.15rem .5rem;cursor:pointer}
566566.delbtn:hover{filter:brightness(1.1)}
567567.modrow{display:flex;align-items:center;gap:.6rem;margin-top:.5rem;font-size:.85rem}
568568.modf{display:inline}
……@@ -571,9 +571,9 @@ details.post>summary{cursor:pointer;color:var(--fg-faint);font-style:italic;list
571571.modw{display:inline}
572572.modw>summary{display:inline;list-style:none;cursor:pointer;color:var(--fg-faint,#888);text-decoration:underline}
573573.modw>summary::-webkit-details-marker{display:none}
574.modw>summary:hover{color:#c92a2a}
575.blockbtn{font:inherit;font-size:.8rem;border:1px solid #c92a2a;background:#c92a2a;color:#fff;border-radius:999px;padding:.2rem .6rem;cursor:pointer;margin-left:.3rem}
576.blockbtn.on{background:none;color:#c92a2a}
574.modw>summary:hover{color:var(--danger)}
575.blockbtn{font:inherit;font-size:.8rem;border:1px solid var(--danger);background:var(--danger);color:#fff;border-radius:999px;padding:.2rem .6rem;cursor:pointer;margin-left:.3rem}
576.blockbtn.on{background:none;color:var(--danger)}
577577.navsearch{margin-left:auto}
578578.navsearch input{font:inherit;font-size:.85rem;padding:.2rem .5rem;border:1px solid var(--line,#e5e5e5);border-radius:999px;background:var(--bg,#fff);color:inherit;width:8rem}
579579.navsearch input:focus{outline:2px solid var(--accent,#1a56db);outline-offset:1px;width:11rem}
……@@ -592,7 +592,7 @@ details.post>summary{cursor:pointer;color:var(--fg-faint);font-style:italic;list
592592.controls label{display:inline-flex;align-items:center;gap:.3rem;min-width:0;font-size:.85rem;color:var(--fg-dim,#5c6672)}
593593/* min-width:0 is the load-bearing half: a flex item defaults to min-width:auto, so a select refuses to
594594 shrink below its longest option and pushes its siblings out of the box instead of wrapping. */
595.controls select{min-width:0;max-width:100%;border:1px solid var(--line,#ccc);border-radius:.3rem;background:var(--bg,#fff);color:inherit}
595.controls select,.controls button{min-width:0;max-width:100%;border:1px solid var(--line);border-radius:var(--r1);background:var(--bg);color:inherit}
596596/* The post button always ends the row, on whatever line it lands on. */
597597#post-btn{margin-left:auto;flex:none}
598598.cc{flex:none}
……@@ -602,7 +602,7 @@ details.post>summary{cursor:pointer;color:var(--fg-faint);font-style:italic;list
602602 discovered after posting; strong enough to be seen, quiet enough not to shout on every page. */
603603/* Staleness. Above everything, because it qualifies everything below it — but a notice, not an alarm:
604604 it is frequently true and correctly ignorable on a P2P node. */
605.stale{font-size:.82rem;background:var(--bg-soft);border:1px solid var(--line);border-left:3px solid #b8860b;border-radius:var(--r1);padding:.45rem .6rem;margin:0 0 var(--s3)}
605.stale{font-size:.82rem;background:var(--bg-soft);border:1px solid var(--line);border-left:3px solid var(--warn);border-radius:var(--r1);padding:.45rem .6rem;margin:0 0 var(--s3)}
606606.composerwho{font-size:.8rem;color:var(--fg-faint,#6b7480);margin:0 0 .4rem}
607607.composerwho strong{color:var(--fg);font-weight:600}
608608@media(max-width:640px){
……@@ -613,7 +613,7 @@ details.post>summary{cursor:pointer;color:var(--fg-faint);font-style:italic;list
613613 #post-btn{margin-left:0;flex:1 1 100%}
614614}
615615.reportbtn{font:inherit;font-size:.85rem;border:none;background:none;color:var(--fg-faint,#6b7480);cursor:pointer;padding:.3rem .35rem;margin:-.3rem -.1rem}
616.reportbtn:hover{color:#c92a2a}
616.reportbtn:hover{color:var(--danger)}
617617.reportbox{display:flex;flex-wrap:wrap;gap:.4rem;align-items:center;margin-top:.4rem;padding:.5rem;border:1px solid var(--line,#e5e5e5);border-radius:var(--r1,.375rem);font-size:.85rem}
618618.reportbox select,.reportbox input,.reportbox button{font:inherit;font-size:.85rem;padding:.25rem .4rem}
619619.reportbox input{flex:1;min-width:8rem}
……@@ -623,14 +623,14 @@ details.post>summary{cursor:pointer;color:var(--fg-faint);font-style:italic;list
623623.listrow:hover{background:var(--bg-soft,#f7f8fa)}
624624.listname{font-weight:600}
625625.listkind{font-size:.75rem;color:var(--fg-faint,#6b7480);border:1px solid var(--line,#e5e5e5);border-radius:999px;padding:.05rem .5rem}
626.listkind.mod{color:#c92a2a;border-color:#c92a2a}
626.listkind.mod{color:var(--danger);border-color:var(--danger)}
627627.listdesc{font-size:.85rem;color:var(--fg-dim,#5c6672);width:100%}
628628.profbanner{margin:0 0 .6rem;border-radius:var(--r2,.625rem);overflow:hidden;background:var(--bg-soft,#f7f8fa)}
629629.profbanner img{display:block;width:100%;aspect-ratio:3/1;object-fit:cover}
630630.profbanner.small img{aspect-ratio:4/1}
631631.bmf{display:inline}
632632.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.bmbtn:hover{color:#f59f00}.bmbtn.on{color:#f59f00}
633.bmbtn:hover{color:var(--bookmark)}.bmbtn.on{color:var(--bookmark)}
634634.bmbtn:active{transform:scale(.88)}
635635.bmbtn:focus-visible{outline:2px solid var(--accent);outline-offset:2px;border-radius:.3rem}
636636/* a11y. A skip link is the difference between reaching the content and tabbing the whole nav on every
……@@ -663,15 +663,15 @@ a:focus-visible,button:focus-visible,input:focus-visible,textarea:focus-visible,
663663.qcancel{font-size:.8rem}
664664.stath{font-size:.85rem;color:var(--fg-dim);margin:1rem 0 .3rem;text-transform:uppercase;letter-spacing:.03em}
665665.statblk{border:1px solid var(--line);border-radius:4px;padding:.5rem .6rem;margin:.4rem 0}
666.statblk.alarm{border-color:#e03131;background:#fff5f5}
666.statblk.alarm{border-color:var(--danger);background:var(--danger-bg)}
667667.statdid{font-size:.8rem;color:var(--fg-dim)}
668668.statsum{font-size:.9rem;margin:.2rem 0 .4rem}
669669.stattbl{border-collapse:collapse;font-size:.8rem;width:100%}
670670.stattbl th{text-align:left;color:var(--fg-faint);font-weight:600;padding:.15rem .4rem .15rem 0}
671671.stattbl td{padding:.15rem .4rem .15rem 0;border-top:1px solid #f1f1f1}
672672.stattbl tr.chosen td{font-weight:600;background:#f8f9ff}
673.alarmnote{color:#c92a2a;font-size:.82rem;margin:.4rem 0 0}
674.ok{color:#2b8a3e}.bad{color:#c92a2a}
673.alarmnote{color:var(--danger);font-size:.82rem;margin:.4rem 0 0}
674.ok{color:var(--ok)}.bad{color:var(--danger)}
675675.ptabs{display:flex;gap:.15rem;border-bottom:1px solid var(--line);margin:.6rem 0 .3rem;flex-wrap:wrap}
676676.ptab{padding:.35rem .7rem;font-size:.88rem;color:var(--fg-dim);text-decoration:none;border-bottom:2px solid transparent}
677677.ptab:hover{color:var(--fg)}
……@@ -693,7 +693,7 @@ a:focus-visible,button:focus-visible,input:focus-visible,textarea:focus-visible,
693693.likebtn,.rpbtn{padding:.3rem .45rem;margin:-.3rem -.1rem}
694694.likebtn:active,.rpbtn:active,.followbtn:active{transform:scale(.88)}
695695.likebtn[disabled],.rpbtn[disabled],.followbtn[disabled]{opacity:.55;cursor:default}
696.likebtn.failed,.rpbtn.failed,.followbtn.failed{color:#c92a2a;animation:shake .25s}
696.likebtn.failed,.rpbtn.failed,.followbtn.failed{color:var(--danger);animation:shake .25s}
697697@keyframes shake{25%{transform:translateX(-2px)}75%{transform:translateX(2px)}}
698698/* Keyboard users get the same affordance the mouse does — these are real buttons, so make focus visible. */
699699.likebtn:focus-visible,.rpbtn:focus-visible,.followbtn:focus-visible,.replyc:focus-visible{outline:2px solid var(--accent);outline-offset:2px;border-radius:.3rem}
……@@ -706,7 +706,7 @@ a:focus-visible,button:focus-visible,input:focus-visible,textarea:focus-visible,
706706.fol{display:inline-block;margin:.1rem .3rem .1rem 0;color:#356}
707707.folmore{color:var(--fg-faint)}
708708.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}
709.cc{font-size:.8rem;color:var(--fg-faint);margin-left:auto}.cc.over{color:var(--danger-text);font-weight:600}
710710.proform label{font-size:.85rem;color:var(--fg-dim)}
711711.proform input,.proform textarea{width:100%;box-sizing:border-box;font:inherit;padding:.4rem;border:1px solid var(--line);border-radius:.3rem}
712712.proform button{font:inherit;padding:.35rem .9rem;cursor:pointer}
……@@ -717,11 +717,10 @@ a:focus-visible,button:focus-visible,input:focus-visible,textarea:focus-visible,
717717.imgprev .thumb img{width:140px;height:100px;object-fit:cover;border-radius:.3rem;background:var(--bg-soft)}
718718.imgprev .thumb input,.imgprev .thumb button{font:inherit;font-size:.75rem;padding:.2rem;width:100%;box-sizing:border-box}
719719.imgprev .thumb button{cursor:pointer}
720@media(prefers-color-scheme:dark){body{background:#111;color:#eee}a,.tag{color:#9ec5ff}.post{border-color:#333}
721.post:hover{background:#191919}.focus .post:hover,details.post.collapsed:not([open]):hover{background:none}
722.controls select,.controls button{background:#222;color:#eee;border:1px solid #444}
723.badge{background:#1e3a5f;color:#9ec5ff}.lbl{background:#4a1e1e;color:#f8b4b4}.meta,.meta .did,.who{color:#aaa}
724.pfp,.pfp.none{background:#374151}.profdid code{color:#9ca3af}.meta .permalink{color:#aaa}.cc{color:#9ca3af}}
720/* The base layer's dark patch lived here: 5 lines hand-repainting body, a/.tag, .post, .post:hover,
721 .controls, .badge, .lbl, .meta/.did/.who/.permalink, .pfp, .profdid and .cc. Every one of those now
722 reads from a token, so the single :root override in the design pass below covers them and this block
723 had nothing left to do. It existed only because those rules were written as literals. */
725724
726725/* ── design pass ──────────────────────────────────────────────────────────────────────────────────
727726 Everything above grew rule-by-rule as features landed and had no shared scale: spacing was ad-hoc,
……@@ -736,6 +735,17 @@ a:focus-visible,button:focus-visible,input:focus-visible,textarea:focus-visible,
736735 --fg:#111; --fg-dim:#5c6672; --fg-faint:#6b7480;
737736 --bg:#fff; --bg-soft:#f7f8fa; --line:#e6e8eb; --line-soft:#f0f1f3;
738737 --accent:#1a56db; --like:#e0245e; --repost:#17bf63;
738 /* Semantic colours. These existed only as literals, which is why the dark patch below had to
739 hand-repaint every element that used one. Measured on --bg #fff: --danger-text 6.47:1 and --lens
740 5.67:1 pass AA for text. Two were corrected on the way in rather than tokenising a known-failing
741 value a second time — --ok was #2b8a3e (4.37:1, text, just under), --bookmark was #f59f00
742 (2.13:1 as an icon, under even the 3:1 non-text bar). --warn is only ever a border, where 3:1
743 applies and 3.25:1 clears it. */
744 --danger:#c92a2a; --danger-text:#b91c1c; --danger-bg:#fde8e8;
745 --warn:#b8860b; --ok:#2a833b; --bookmark:#c77800; --badge-bg:#e8f0fe;
746 /* The lens family names a FOREIGN source — content this node relays but does not hold. Audit #45
747 lists that distinction as load-bearing, so it stays a visible treatment, not a hint. */
748 --lens:#3b5bdb; --lens-line:#cbd5e1; --lens-edge:#dbe4ff;
739749 --s1:.25rem; --s2:.5rem; --s3:.75rem; --s4:1rem; --s5:1.5rem; --s6:2rem;
740750 /* Radii and type land on whole pixels at a 16px root. The previous values (6/10px, and
741751 12.48/13.6/15.2/16.8/21.6px) were on no grid at all, which is what fractional rem buys you:
……@@ -748,7 +758,7 @@ a:focus-visible,button:focus-visible,input:focus-visible,textarea:focus-visible,
748758}
749759body{font-family:ui-sans-serif,system-ui,-apple-system,"Segoe UI",Roboto,sans-serif;
750760 max-width:640px;margin:0 auto;padding:var(--s5) var(--s4) var(--s6);color:var(--fg);
751 line-height:1.55;-webkit-font-smoothing:antialiased;font-size:var(--f2)}
761 line-height:1.55;-webkit-font-smoothing:antialiased;font-size:var(--f2);background:var(--bg)}
752762h1{display:flex;align-items:center;gap:var(--s2);font-size:var(--f4);letter-spacing:-.01em;margin:0 0 var(--s3)}
753763h1 small{font-size:var(--f1)}
754764/* The search form is a fixed 8rem and a flex item defaults to min-width:auto, so on a nowrap row it
……@@ -816,7 +826,13 @@ nav a[aria-current=page]{color:var(--fg);font-weight:600;border-bottom-color:var
816826}
817827@media(prefers-color-scheme:dark){
818828 :root{--fg:#e8eaed;--fg-dim:#9aa4b2;--fg-faint:#79838f;--bg:#111;--bg-soft:#191b1f;
819 --line:#2b2f36;--line-soft:#23262b;--accent:#9ec5ff}
829 --line:#2b2f36;--line-soft:#23262b;--accent:#9ec5ff;
830 /* Measured on --bg #111: danger-text 10.95:1, ok 10.84:1, bookmark 8.85:1, lens 9.47:1.
831 --danger keeps its light value because it is a fixed button background that white sits on
832 (5.46:1) rather than something read against the page. */
833 --danger:#c92a2a;--danger-text:#f8b4b4;--danger-bg:#4a1e1e;
834 --warn:#e3b341;--ok:#4ade80;--bookmark:#f59f00;--badge-bg:#1e3a5f;
835 --lens:#a5b4fc;--lens-line:#3b4252;--lens-edge:#2d3a5f}
820836 .post .media img{border-color:var(--line)}
821837 .composer textarea{background:#0e0f12;color:var(--fg)}
822838 #post-btn{background:var(--accent);color:#0b0d10}
package.json +1 -1
……@@ -1,6 +1,6 @@
11{
22 "name": "favela-web",
3 "version": "0.3.67",
3 "version": "0.3.68",
44 "description": "favela-web — the web/SSR surface of Favela, the native P2P-local, community-owned social client + appview over the self-custody atproto stack (hyper-proto-peer). One of the Favela surfaces (alongside favela-cli and favela-native). bsky is an optional POSSE export, not the home. See DESIGN.md.",
55 "type": "module",
66 "main": "bin/favela-web.js",