OverviewFilesCommitsIssuesDocsReleasesPeers

favela-web commits / c4bbf43

render: put the base layer's 59 font sizes on the scale, and state the hit floor
Phase 1b snapped the TOKENS but the base layer still set type as raw rem, so most of the app never read the scale it defined: 13 distinct values across 59 rules, including 14.4px, 12.8px and 13.1px. Each is mapped to its nearest step at a 16px root (.7/.72/.75/.78/.8 -> --f0, .82/.85/.88/.9/.92 -> --f1, .95 -> --f2, 1.15 -> --f3, 1.4 -> --f4). Measured: every font-size rendered on the timeline is now 12, 14, 16 or 22px. No fractional sizes remain. That change caught a live one. The like/repost hit area was never declared — it fell out of glyph size plus padding, so taking the glyph from 14.4px to 14px silently shrank the control from 24px to 23.59px, crossing below WCAG 2.2 AA 2.5.8 (24x24). A target size that moves when someone edits a font scale is not a target size, so it is now stated: min-width/min-height 24px on an inline-flex box, measured back at exactly 24px. 24px is the FLOOR, not the goal — Material asks 48dp, Apple 44pt, and bsky applies an explicit hitSlop to every control. The post-row rebuild raises it. Two corrections to my own audit while I was in here, both from reading one rule and missing a later override: the interaction controls DO render (they are gated on a signed-in viewer via withViewer, and I had been looking at anonymous pages), and their padding was never the ~2.4px I reported — line 693 overrides it. Completes Phase 1. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
c4bbf4315ceae7d8a0203873b885d7894403ef6c
by plankatron <fleeky@gmail.com> · 2026-08-24T14:38:06.000Z

Changed files (2) +66 -61

lib/render.js +65 -60
……@@ -482,8 +482,8 @@ export function countThread (replies) {
482482// ── page shell ────────────────────────────────────────────────
483483const STYLE = `
484484body{font-family:system-ui,sans-serif;max-width:620px;margin:2rem auto;padding:0 1rem;color:var(--fg)}
485a{color:var(--accent)}h1{font-size:1.4rem}h1 small{color:var(--fg-faint);font-weight:400}
486nav{margin:.4rem 0 1rem;display:flex;gap:1rem;font-size:.9rem}
485a{color:var(--accent)}h1{font-size:var(--f4)}h1 small{color:var(--fg-faint);font-weight:400}
486nav{margin:.4rem 0 1rem;display:flex;gap:1rem;font-size:var(--f1)}
487487.post{position:relative;border-bottom:1px solid var(--line);padding:.8rem 0}.post .text{white-space:pre-wrap}
488488/* The whole-entry link emitted by renderPost. It covers the card and sits UNDER everything, so the
489489 card is one big click target for "open this thread". */
……@@ -505,39 +505,39 @@ nav{margin:.4rem 0 1rem;display:flex;gap:1rem;font-size:.9rem}
505505.focus .post:hover,details.post.collapsed:not([open]):hover{background:none}
506506.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)}
507507details.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)}
508.meta{color:var(--fg-faint);font-size:var(--f0);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: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)}
510.badge{display:inline-block;font-size:var(--f0);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:var(--f0);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}
514.who{font-size:.85rem;color:var(--fg-faint);margin-left:auto}
514.who{font-size:var(--f1);color:var(--fg-faint);margin-left:auto}
515515.phead{display:flex;align-items:center;gap:.5rem;flex-wrap:wrap;margin-bottom:.35rem}
516516.byline{display:flex;align-items:center;gap:.5rem;text-decoration:none;color:inherit;min-width:0}
517517.byline:hover .name{text-decoration:underline}
518.byline .name{font-weight:600;font-size:.95rem;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
518.byline .name{font-weight:600;font-size:var(--f2);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
519519.pfp{width:36px;height:36px;border-radius:50%;object-fit:cover;background:var(--line);flex:none}
520520.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}
524524.profhdr{display:flex;gap:.9rem;align-items:flex-start;margin:.6rem 0 1rem}
525.profname{font-size:1.15rem;font-weight:700}
526.profdid code{font-size:.75rem;color:var(--fg-faint);word-break:break-all}
525.profname{font-size:var(--f3);font-weight:700}
526.profdid code{font-size:var(--f0);color:var(--fg-faint);word-break:break-all}
527527.profbio{margin:.4rem 0 0;white-space:pre-wrap}
528.aturi code{font-size:.75rem;word-break:break-all}
528.aturi code{font-size:var(--f0);word-break:break-all}
529529.replyc{color:var(--fg-faint);text-decoration:none}.replyc:hover{text-decoration:underline}
530530.rpf{display:inline;margin-left:.15rem}
531.rpbtn{font:inherit;font-size:.9rem;line-height:1;border:none;background:none;color:var(--fg-faint);cursor:pointer;padding:0 .15rem}
531.rpbtn{font:inherit;font-size:var(--f1);line-height:1;border:none;background:none;color:var(--fg-faint);cursor:pointer;padding:0 .15rem}
532532.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}
533.rps{color:var(--fg-faint);font-size:var(--f0)}
534.inreply{font-size:var(--f0);color:var(--fg-faint);margin-bottom:.25rem}
535535.replyf{margin:.8rem 0;padding:.6rem;border:1px solid var(--line);border-radius:.5rem}
536536.replyf textarea{width:100%;box-sizing:border-box;font:inherit;padding:.4rem;border:1px solid var(--line);border-radius:.3rem}
537537.replyf p{display:flex;align-items:center;gap:.5rem;margin:.4rem 0 0}
538538.replyf button{font:inherit;padding:.3rem .9rem;cursor:pointer}
539539.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}
540.thread h3{font-size:var(--f1);color:var(--fg-dim);margin:.3rem 0}
541541/* Nested replies: children live INSIDE the parent node, so this one rule gives the accumulating indent
542542 and the thread line at every depth. Capped padding keeps a deep chain from marching off-screen. */
543543.tnode .tnode{margin-left:.5rem;padding-left:.7rem;border-left:2px solid var(--line-soft)}
……@@ -548,48 +548,48 @@ details.post>summary{cursor:pointer;color:var(--fg-faint);font-style:italic;list
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. */
550550.lensup{margin:.3rem 0 .6rem;border-top:0;border-bottom:1px dashed var(--lens-line);padding:0 0 .4rem}
551.lens h3{font-size:.85rem;color:var(--fg-dim);font-weight:600;margin:.3rem 0}
551.lens h3{font-size:var(--f1);color:var(--fg-dim);font-weight:600;margin:.3rem 0}
552552.lensname{color:var(--lens)}
553553.lpost{border-left:2px solid var(--lens-edge);padding:.3rem 0 .3rem .7rem;margin:.4rem 0}
554.lwho{font-size:.9rem;font-weight:600}
554.lwho{font-size:var(--f1);font-weight:600}
555555.lhandle{font-weight:400;color:var(--fg-faint)}
556556.ltext{white-space:pre-wrap;margin:.15rem 0}
557.lensoff{font-size:.78rem;margin-top:1rem}
558.lensnote{font-size:.8rem;color:var(--fg-dim);margin:.4rem 0 0}
557.lensoff{font-size:var(--f0);margin-top:1rem}
558.lensnote{font-size:var(--f0);color:var(--fg-dim);margin:.4rem 0 0}
559559.qbtn{color:var(--fg-faint);text-decoration:none}.qbtn:hover{color:var(--accent)}
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}
563563.delw>summary:hover{color:var(--danger)}
564564.delf{display:inline;margin-left:.3rem}
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}
565.delbtn{font:inherit;font-size:var(--f0);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)}
567.modrow{display:flex;align-items:center;gap:.6rem;margin-top:.5rem;font-size:.85rem}
567.modrow{display:flex;align-items:center;gap:.6rem;margin-top:.5rem;font-size:var(--f1)}
568568.modf{display:inline}
569.mutebtn{font:inherit;font-size:.85rem;border:none;background:none;color:var(--fg-faint,#888);cursor:pointer;padding:.2rem 0;text-decoration:underline}
569.mutebtn{font:inherit;font-size:var(--f1);border:none;background:none;color:var(--fg-faint,#888);cursor:pointer;padding:.2rem 0;text-decoration:underline}
570570.mutebtn:hover{color:var(--fg,#111)}.mutebtn.on{color:var(--fg-dim,#666)}
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}
574574.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}
575.blockbtn{font:inherit;font-size:var(--f0);border:1px solid var(--danger);background:var(--danger);color:#fff;border-radius:999px;padding:.2rem .6rem;cursor:pointer;margin-left:.3rem}
576576.blockbtn.on{background:none;color:var(--danger)}
577577.navsearch{margin-left:auto}
578.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}
578.navsearch input{font:inherit;font-size:var(--f1);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}
580580.searchbox{display:flex;gap:.5rem;margin:.6rem 0 1rem}
581581.searchbox input{flex:1;font:inherit;padding:.45rem .7rem;border:1px solid var(--line,#e5e5e5);border-radius:999px;background:var(--bg,#fff);color:inherit}
582582.searchbox button{font:inherit;padding:.4rem 1rem;border:none;border-radius:999px;background:var(--accent,#1a56db);color:var(--bg);font-weight:600;cursor:pointer}
583583.actorrow{padding:.4rem 0;border-bottom:1px solid var(--line-soft,#f0f1f3)}
584.gatenote{font-size:.85rem;color:var(--fg-dim,#666);margin:.6rem 0 0}
585.gateoff{font-size:.85rem;margin:.6rem 0 0}
586.draftnote{font-size:.8rem;color:var(--fg-dim,#5c6672);margin:0 0 .3rem}
584.gatenote{font-size:var(--f1);color:var(--fg-dim,#666);margin:.6rem 0 0}
585.gateoff{font-size:var(--f1);margin:.6rem 0 0}
586.draftnote{font-size:var(--f0);color:var(--fg-dim,#5c6672);margin:0 0 .3rem}
587587/* The composer control row. It was a single non-wrapping flex line, which was fine with ONE select and
588588 broke once scope/lang/quotes/replies were all added: the row overflowed its container and the image
589589 picker and post button ended up outside it. Four dropdowns is simply more than one line holds at 640px,
590590 and less on a phone — so it wraps, and every item is allowed to shrink. */
591591.controls{flex-wrap:wrap;gap:.4rem .7rem;row-gap:.5rem}
592.controls label{display:inline-flex;align-items:center;gap:.3rem;min-width:0;font-size:.85rem;color:var(--fg-dim,#5c6672)}
592.controls label{display:inline-flex;align-items:center;gap:.3rem;min-width:0;font-size:var(--f1);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. */
595595.controls select,.controls button{min-width:0;max-width:100%;border:1px solid var(--line);border-radius:var(--r1);background:var(--bg);color:inherit}
……@@ -597,13 +597,13 @@ details.post>summary{cursor:pointer;color:var(--fg-faint);font-style:italic;list
597597#post-btn{margin-left:auto;flex:none}
598598.cc{flex:none}
599599#img-controls{flex:none}
600.composerhint{font-size:.78rem;color:var(--fg-faint,#6b7480);margin:.4rem 0 0}
600.composerhint{font-size:var(--f0);color:var(--fg-faint,#6b7480);margin:.4rem 0 0}
601601/* Which identity this form writes as. Above the textarea because it has to be read BEFORE typing, not
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 var(--warn);border-radius:var(--r1);padding:.45rem .6rem;margin:0 0 var(--s3)}
606.composerwho{font-size:.8rem;color:var(--fg-faint,#6b7480);margin:0 0 .4rem}
605.stale{font-size:var(--f1);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)}
606.composerwho{font-size:var(--f0);color:var(--fg-faint,#6b7480);margin:0 0 .4rem}
607607.composerwho strong{color:var(--fg);font-weight:600}
608608@media(max-width:640px){
609609 /* On a phone the labels alone fill a line, so give the selects room and let each pair own its width. */
……@@ -612,24 +612,24 @@ details.post>summary{cursor:pointer;color:var(--fg-faint);font-style:italic;list
612612 .controls select{flex:1 1 auto}
613613 #post-btn{margin-left:0;flex:1 1 100%}
614614}
615.reportbtn{font:inherit;font-size:.85rem;border:none;background:none;color:var(--fg-faint,#6b7480);cursor:pointer;padding:.3rem .35rem;margin:-.3rem -.1rem}
615.reportbtn{font:inherit;font-size:var(--f1);border:none;background:none;color:var(--fg-faint,#6b7480);cursor:pointer;padding:.3rem .35rem;margin:-.3rem -.1rem}
616616.reportbtn:hover{color:var(--danger)}
617.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}
618.reportbox select,.reportbox input,.reportbox button{font:inherit;font-size:.85rem;padding:.25rem .4rem}
617.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:var(--f1)}
618.reportbox select,.reportbox input,.reportbox button{font:inherit;font-size:var(--f1);padding:.25rem .4rem}
619619.reportbox input{flex:1;min-width:8rem}
620620.lmedia{display:flex;gap:.4rem;flex-wrap:wrap;margin-top:.35rem}
621621.lthumb{width:110px;height:110px;object-fit:cover;border-radius:var(--r1,.375rem);background:var(--bg-soft,#f3f4f6)}
622622.listrow{display:flex;align-items:center;gap:.6rem;flex-wrap:wrap;padding:.6rem 0;border-bottom:1px solid var(--line,#e5e5e5);text-decoration:none;color:inherit}
623623.listrow:hover{background:var(--bg-soft,#f7f8fa)}
624624.listname{font-weight:600}
625.listkind{font-size:.75rem;color:var(--fg-faint,#6b7480);border:1px solid var(--line,#e5e5e5);border-radius:999px;padding:.05rem .5rem}
625.listkind{font-size:var(--f0);color:var(--fg-faint,#6b7480);border:1px solid var(--line,#e5e5e5);border-radius:999px;padding:.05rem .5rem}
626626.listkind.mod{color:var(--danger);border-color:var(--danger)}
627.listdesc{font-size:.85rem;color:var(--fg-dim,#5c6672);width:100%}
627.listdesc{font-size:var(--f1);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}
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}
632.bmbtn{font:inherit;font-size:var(--f1);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}
633633.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}
……@@ -645,42 +645,42 @@ main:focus{outline:none}
645645a:focus-visible,button:focus-visible,input:focus-visible,textarea:focus-visible,select:focus-visible,summary:focus-visible{outline:2px solid var(--accent,#1a56db);outline-offset:2px;border-radius:.2rem}
646646.quote{display:block;border:1px solid var(--line,#e5e5e5);border-radius:var(--r2,.625rem);padding:.5rem .6rem;margin-top:.5rem;text-decoration:none;color:inherit}
647647.quote:hover{background:var(--bg-soft,#fafafa)}
648.qhead{display:flex;align-items:center;gap:.35rem;font-size:.85rem;color:var(--fg-dim,#666)}
648.qhead{display:flex;align-items:center;gap:.35rem;font-size:var(--f1);color:var(--fg-dim,#666)}
649649.qname{font-weight:600;color:var(--fg,#111)}
650650.qpfp{width:20px;height:20px}
651.qtext{display:block;margin-top:.25rem;white-space:pre-wrap;font-size:.9rem}
652.missingq{color:var(--fg-faint,#888);font-style:italic;font-size:.85rem}
651.qtext{display:block;margin-top:.25rem;white-space:pre-wrap;font-size:var(--f1)}
652.missingq{color:var(--fg-faint,#888);font-style:italic;font-size:var(--f1)}
653653.card{display:flex;gap:.6rem;align-items:stretch;border:1px solid var(--line,#e5e5e5);border-radius:var(--r2,.625rem);overflow:hidden;margin-top:.5rem;text-decoration:none;color:inherit}
654654.card:hover{background:var(--bg-soft,#fafafa)}
655655.cardimg{width:88px;height:88px;object-fit:cover;flex:none}
656656.cardtxt{display:flex;flex-direction:column;justify-content:center;padding:.5rem .6rem;min-width:0}
657.cardhost{font-size:.72rem;color:var(--fg-faint,#888);text-transform:lowercase}
658.cardtitle{font-weight:600;font-size:.9rem;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
659.carddesc{font-size:.82rem;color:var(--fg-dim,#666);display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden}
657.cardhost{font-size:var(--f0);color:var(--fg-faint,#888);text-transform:lowercase}
658.cardtitle{font-weight:600;font-size:var(--f1);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
659.carddesc{font-size:var(--f1);color:var(--fg-dim,#666);display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden}
660660.vid{width:100%;max-height:70vh;border-radius:var(--r2,.625rem);background:#000;display:block}
661.qpreview{font-size:.85rem;color:var(--fg-dim,#666);border-left:3px solid var(--accent,#1a56db);padding:.2rem .5rem;margin-bottom:.5rem}
661.qpreview{font-size:var(--f1);color:var(--fg-dim,#666);border-left:3px solid var(--accent,#1a56db);padding:.2rem .5rem;margin-bottom:.5rem}
662662.qptext{font-style:italic}
663.qcancel{font-size:.8rem}
664.stath{font-size:.85rem;color:var(--fg-dim);margin:1rem 0 .3rem;text-transform:uppercase;letter-spacing:.03em}
663.qcancel{font-size:var(--f0)}
664.stath{font-size:var(--f1);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}
666666.statblk.alarm{border-color:var(--danger);background:var(--danger-bg)}
667.statdid{font-size:.8rem;color:var(--fg-dim)}
668.statsum{font-size:.9rem;margin:.2rem 0 .4rem}
669.stattbl{border-collapse:collapse;font-size:.8rem;width:100%}
667.statdid{font-size:var(--f0);color:var(--fg-dim)}
668.statsum{font-size:var(--f1);margin:.2rem 0 .4rem}
669.stattbl{border-collapse:collapse;font-size:var(--f0);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:var(--danger);font-size:.82rem;margin:.4rem 0 0}
673.alarmnote{color:var(--danger);font-size:var(--f1);margin:.4rem 0 0}
674674.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}
676.ptab{padding:.35rem .7rem;font-size:.88rem;color:var(--fg-dim);text-decoration:none;border-bottom:2px solid transparent}
676.ptab{padding:.35rem .7rem;font-size:var(--f1);color:var(--fg-dim);text-decoration:none;border-bottom:2px solid transparent}
677677.ptab:hover{color:var(--fg)}
678678.ptab.on{color:var(--fg);font-weight:600;border-bottom-color:#444}
679679.ancestor{opacity:.72}
680.missing{font-size:.8rem;border-left:2px dashed var(--line);padding-left:.6rem}
681.likes{color:var(--fg-faint);font-size:.8rem}
680.missing{font-size:var(--f0);border-left:2px dashed var(--line);padding-left:.6rem}
681.likes{color:var(--fg-faint);font-size:var(--f0)}
682682.likef{display:inline;margin-left:.5rem}
683.likebtn{font:inherit;font-size:.9rem;line-height:1;border:none;background:none;color:var(--fg-faint);cursor:pointer;padding:0 .15rem}
683.likebtn{font:inherit;font-size:var(--f1);line-height:1;border:none;background:none;color:var(--fg-faint);cursor:pointer;padding:0 .15rem}
684684.likebtn.on{color:var(--like)}.likebtn:hover{color:var(--like)}
685685/* Responsive controls. These used to be plain form POSTs that reloaded the whole page to toggle a heart —
686686 correct, and it felt broken. /static/actions.js now posts JSON and updates in place; this is the part
……@@ -690,7 +690,12 @@ a:focus-visible,button:focus-visible,input:focus-visible,textarea:focus-visible,
690690 - .failed briefly reddens a control whose write did NOT land, because the optimistic flip is rolled
691691 back and a silent revert is indistinguishable from never having clicked. */
692692.likebtn,.rpbtn,.followbtn{transition:transform .08s ease,color .12s ease,opacity .12s ease;touch-action:manipulation}
693.likebtn,.rpbtn{padding:.3rem .45rem;margin:-.3rem -.1rem}
693/* The hit area is stated, not inherited. It used to fall out of glyph size plus padding, which meant
694 dropping the font from 14.4px to 14px silently took it to 23.59px — under WCAG 2.2 AA 2.5.8, which
695 asks 24x24. A target size that moves when someone edits a font scale is not a target size. This is
696 the FLOOR, not the goal: Material asks 48dp and Apple 44pt, and the post-row rebuild raises it. */
697.likebtn,.rpbtn{display:inline-flex;align-items:center;justify-content:center;
698 min-width:24px;min-height:24px;padding:.3rem .45rem;margin:-.3rem -.1rem}
694699.likebtn:active,.rpbtn:active,.followbtn:active{transform:scale(.88)}
695700.likebtn[disabled],.rpbtn[disabled],.followbtn[disabled]{opacity:.55;cursor:default}
696701.likebtn.failed,.rpbtn.failed,.followbtn.failed{color:var(--danger);animation:shake .25s}
……@@ -702,20 +707,20 @@ a:focus-visible,button:focus-visible,input:focus-visible,textarea:focus-visible,
702707.followbtn{font:inherit;border:1px solid var(--accent);background:var(--accent);color:var(--bg);border-radius:.4rem;padding:.3rem .9rem;cursor:pointer}
703708.followbtn.on{background:none;color:var(--accent)}
704709.scopenote{color:var(--fg-faint);font-weight:400}
705.profnet{margin:.3rem 0;font-size:.92rem}
710.profnet{margin:.3rem 0;font-size:var(--f1)}
706711.fol{display:inline-block;margin:.1rem .3rem .1rem 0;color:#356}
707712.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:var(--danger-text);font-weight:600}
710.proform label{font-size:.85rem;color:var(--fg-dim)}
713.profgraph{margin:.4rem 0 0;font-size:var(--f1);color:var(--fg-dim)}
714.cc{font-size:var(--f0);color:var(--fg-faint);margin-left:auto}.cc.over{color:var(--danger-text);font-weight:600}
715.proform label{font-size:var(--f1);color:var(--fg-dim)}
711716.proform input,.proform textarea{width:100%;box-sizing:border-box;font:inherit;padding:.4rem;border:1px solid var(--line);border-radius:.3rem}
712717.proform button{font:inherit;padding:.35rem .9rem;cursor:pointer}
713718.proform .cc{margin-left:0;display:inline-block;margin-top:.15rem}
714.imgpick{display:inline-block;cursor:pointer;border:1px solid var(--line);border-radius:.3rem;padding:.25rem .6rem;font-size:.85rem}
719.imgpick{display:inline-block;cursor:pointer;border:1px solid var(--line);border-radius:.3rem;padding:.25rem .6rem;font-size:var(--f1)}
715720.imgprev{display:flex;gap:.5rem;flex-wrap:wrap;margin-top:.5rem}
716721.imgprev .thumb{display:flex;flex-direction:column;gap:.2rem;width:140px}
717722.imgprev .thumb img{width:140px;height:100px;object-fit:cover;border-radius:.3rem;background:var(--bg-soft)}
718.imgprev .thumb input,.imgprev .thumb button{font:inherit;font-size:.75rem;padding:.2rem;width:100%;box-sizing:border-box}
723.imgprev .thumb input,.imgprev .thumb button{font:inherit;font-size:var(--f0);padding:.2rem;width:100%;box-sizing:border-box}
719724.imgprev .thumb button{cursor:pointer}
720725/* The base layer's dark patch lived here: 5 lines hand-repainting body, a/.tag, .post, .post:hover,
721726 .controls, .badge, .lbl, .meta/.did/.who/.permalink, .pfp, .profdid and .cc. Every one of those now
package.json +1 -1
……@@ -1,6 +1,6 @@
11{
22 "name": "favela-web",
3 "version": "0.3.69",
3 "version": "0.3.70",
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",