/* app-feeds.css — DENSITY, the meme galaxy, QUEUE v2, JUICE v2, the collective, HOT and comment QOL.

   ⚖ ONE STYLESHEET, TEN PARTS, AND THE ORDER IS THE CASCADE. This was web/app.css, a single
   5,357-line file, until the 2026-08-05 split (it held lines 2429-2999). The parts must be
   linked in this order on every page:
     app.css → app-rank.css → app-curation.css → app-moderation.css → app-profile.css → app-feeds.css → app-tagrows.css → app-skins.css → app-builder.css → app-viewer.css
   That is not tidiness, it is the specificity tie-break: two rules of equal weight are
   resolved by which one comes LAST, and this file's own sections rely on it — "THE LAST WORD
   ON MODIFIER VERBS" (in app-viewer.css) says in its heading that it must be the final say.
   Re-ordering the <link> tags re-orders the cascade and changes what the site looks like,
   silently, on whichever surface the tie mattered. server/tests/cssparts.test.js pins the
   order on all 18 pages.

   ⚠ The xp-*.css files load AFTER every part, exactly as they loaded after the whole of
   app.css before. Do not insert a part below them. */
/* =====================================================================
   DENSITY v1 (docs/SPEC_density.md) — the compacted post page. A glyph
   meta-strip + disclosure sections + an action bar with a ⋯ overflow.
   Visual target: docs/design/post_density_mockup.html, mapped onto the
   terminal theme tokens. Reduced-motion gates the folds to instant.
   ===================================================================== */
.post{display:grid;grid-template-columns:minmax(0,1fr) 356px;gap:20px;padding:16px 0}
@media(max-width:820px){.post{grid-template-columns:1fr}}
/* COMMENTS SIT DIRECTLY UNDER THE POST (owner ruling 2026-07-29, re-aimed 2026-08-06).
   The 2026-07-29 ruling moved them out of the 356px control column — at depth 6 the indent left
   almost no text column — and spanned them `1 / -1` across the whole underside. His words then
   were the real target: *"the entire area under the post sat empty"*.

   ⚠ SPANNING DID NOT ACHIEVE THAT, and the owner's screenshot on 2026-08-06 is what it looks
   like when it fails: a post with 11 tags makes the CONTROL COLUMN the tallest thing in row 1,
   the media column has nothing to fill that height with, and the comments — living in row 2 —
   wait for the sidebar to finish. The result is ~600px of void directly under the post, which
   is the exact complaint from a week earlier, restored by the fix for it.

   So the sidebar SPANS BOTH ROWS and the comments take column 1, row 2: they now begin under
   the action bar no matter how tall the tools column gets.
   ⚠ `min-content 1fr` IS LOad-BEARING, and it is the part that is easy to get wrong. With two
   INTRINSIC tracks (`auto`, `max-content`) the grid algorithm distributes a row-spanning item's
   excess height across every intrinsic track it crosses — so the sidebar re-inflated row 1 and
   the gap came back, smaller. An `fr` track is not intrinsic: the excess lands there instead,
   and row 1 hugs the post. Measured in a harness against these exact rules, not reasoned about.
   THE COST, stated plainly: comments are as wide as the MEDIA column now, not the full page.
   They are still ~1,100px at 1440 and they are where the eye lands after the post, which is
   what the ruling was actually asking for. */
.post>.postcomments{grid-column:1;grid-row:2;min-width:0}
@media(min-width:821px){
  .post{grid-template-rows:min-content 1fr}
  .post>.ctl:not(.postcomments){grid-column:2;grid-row:1 / span 2}
}
/* Below the collapse there is one column and nothing to span: the grid stacks in DOM order
   (post, tools, comments), which is already right — the phone reads comments in the viewer. */
@media(max-width:820px){.post>.postcomments{grid-column:1;grid-row:auto}}
/* NO BOX (owner ruling 2026-07-29: "remove the box it sits in, I want a cleaner UI here").
   The panel border + fill are what make a 356px SIDEBAR section read as a distinct card. At
   full width that frame is just a rectangle drawn around the page, so it goes — the header
   and the thread's own depth bars carry the structure now. Scoped to .postcomments, so every
   other .ctl .sec keeps its card. */
.post>.postcomments>.sec{border:0;background:none}
.post>.postcomments .sh{padding-left:0;padding-right:0}
.post>.postcomments .inner{padding-left:0;padding-right:0}
.postmain{min-width:0}
.stagewrap{min-width:0}
.stagehint{display:flex;align-items:center;justify-content:space-between;gap:10px;margin-top:4px}
.stagehint .sh-text{margin:0}
.stagehint .sh-badge{font-size:10px;letter-spacing:calc(.14em * var(--track));text-transform:var(--caps);color:var(--accent);
  border:1px solid var(--accent-deep);padding:1px 6px;white-space:nowrap}
.ctl{display:flex;flex-direction:column;gap:10px}

/* action bar */
.actionbarwrap{margin-top:12px}
.actionbar{display:flex;align-items:center;gap:8px;flex-wrap:wrap}
.actionbar .barmsg,.barmsg{min-height:1em;margin:6px 0 0}
.vote{display:inline-flex;align-items:center;gap:8px;border:1px solid var(--hair-hi);
  background:var(--panel);padding:4px 8px}
.vote .vbtn{position:relative;border:0;background:none;color:var(--faint);font-size:15px;line-height:1;padding:2px 4px;cursor:pointer}
/* ⚖ 18x19 DRAWN, 18x44 TAPPED (forage-fxj.1, 2026-08-21). MEASURED on /post at 390x844 with
   touch: the ▲ and ▼ of the post's own vote, inside a 91x29 `.vote` pill. The pill is the drawn
   thing and it stays — growing the triangles fattens the whole action bar, which is what
   docs/DECISIONS.md "The floor may not change what a member can see" refuses. Vertically there
   is room: the bar wraps at `gap:8px` and the nearest control below MEASURED 391px against this
   box's 391.5px bottom edge, and it sits at x 340-376 while these two are at x 23-97.
   ⚠ 18px WIDE IS THE RESIDUAL AND IT IS NOT AN OVERSIGHT. ▲ ends at x=41, `.vote .vnet` (`+0`,
   a span with no handler) runs 49-71, ▼ starts at 79 — so a horizontal halo big enough to
   matter would cross the net and put DOWNVOTE under a thumb aiming at UPVOTE. That is the one
   mis-tap in this bar that cannot be undone by tapping again, because the second tap lands on
   the same wrong triangle. Vertical only, waived at the measured width. */
@media(pointer:coarse){
  .vote .vbtn::after{content:"";position:absolute;left:0;right:0;top:50%;transform:translateY(-50%);
    height:var(--tap-min)}
}
.vote .vbtn:hover:not(:disabled){color:var(--ink)}
.vote .vbtn:disabled{opacity:.5;cursor:default}
.vote .vbtn.up.on{color:var(--good)} .vote .vbtn.down.on{color:var(--bad)}
.vote .vnet{color:var(--ink);font-weight:600;min-width:2.2em;text-align:center}
.act{display:inline-flex;align-items:center;gap:6px;justify-content:center;
  background:var(--panel);border:1px solid var(--hair-hi);color:var(--dim);
  height:32px;min-width:34px;padding:0 10px;cursor:pointer;font-size:14px}
.act:hover{border-color:var(--accent-deep);color:var(--ink)}
.act .g{font-size:15px;line-height:1}
.act .c{color:var(--faint);font-size:12px}
.act.push{margin-left:auto}
/* FAVORITES (owner ruling 2026-07-20): the private save toggle. Saved = accent star
   (unmistakable "you saved this"); the label/glyph reuse the shared .act idiom. */
.act.fav.on{border-color:var(--accent);color:var(--accent)}
.act.fav.on .g,.act.fav.on .c{color:var(--accent)}

/* overflow menu */
.ovwrap{position:relative;display:inline-flex}
.ovwrap.push{margin-left:auto} /* hugs the actionbar's right edge — see overflowMenu() for why this can't live on .ov-btn */
.ovmenu{position:absolute;right:0;top:calc(100% + 6px);z-index:var(--z-menu);display:none;flex-direction:column;
  min-width:200px;background:var(--sunk);border:1px solid var(--hair-hi);padding:4px;box-shadow:0 8px 24px rgba(0,0,0,.7)}
.ovmenu.open{display:flex}
.ov-item{display:flex;align-items:center;gap:9px;text-align:left;background:none;border:0;
  color:var(--dim);font-size:12.5px;padding:8px 9px;cursor:pointer}
.ov-item:hover{background:var(--panel);color:var(--ink)}
/* ⚖ 190x35 -> 190x44, AND THIS ONE IS A REAL min-height, NOT A HIT AREA (forage-fxj, 2026-08-21).
   MEASURED in the real app at 390x844 with touch, the ⋯ menu open on /post: seven rows, 35px
   (55px where the label wraps), **0px apart — they TILE**. A tiling list is the one shape an
   overhang cannot serve: every pixel it grows into belongs to the row above or below, and the
   rows either side of `purge file (permanent)` are `soft-hide` and `crop`. So the ROW grows.
   ⚠ THAT IS ALLOWED HERE and it is not a hole in "the floor may not change what a member can
   see": that ruling is about a control's drawn CHROME — a button whose 26px band became 44px of
   fill. A menu row has no chrome; it is a line of text in a list, and 44px is what a list row is.
   The menu is `position:absolute`, so the 63px it adds costs the page no layout at all. */
@media(pointer:coarse){ .ov-item{min-height:var(--tap-min)} }
.ov-item .ov-g{color:var(--faint);width:14px;text-align:center;flex:0 0 auto}
.ov-item.danger{color:var(--bad)} .ov-item.danger:hover{background:#2a1614;color:var(--bad)}
.ov-div{height:1px;background:var(--hair);margin:4px 2px}

/* meta strip */
.metastrip{display:flex;flex-wrap:wrap;gap:6px;border:1px solid var(--hair);background:var(--panel);padding:9px 10px}
.metastrip .mi{display:inline-flex;align-items:center;gap:5px;font-size:12px;color:var(--dim);position:relative;padding:2px}
.metastrip .mi-g{color:var(--faint);font-size:13px}
.metastrip .mi-g.ok{color:var(--good)}
.metastrip .mi-v{color:var(--ink);font-weight:500}
.metastrip .mi-t{color:var(--dim)}
.metastrip .mi .ulink{color:var(--ink)}
.metastrip .mi:hover{color:var(--ink)}
.metastrip .mi[data-tip]:hover::after{content:attr(data-tip);position:absolute;bottom:calc(100% + 6px);left:0;
  white-space:nowrap;background:var(--void);border:1px solid var(--hair-hi);color:var(--dim);
  font-size:10px;letter-spacing:calc(.06em * var(--track));padding:3px 6px;z-index:var(--z-tooltip);text-transform:var(--caps)}

/* disclosure sections */
.ctl .sec{border:1px solid var(--hair);background:var(--panel);overflow:hidden}
.ctl .sh{display:flex;align-items:center;gap:9px;width:100%;background:none;border:0;color:var(--dim);
  font-size:11px;letter-spacing:calc(.15em * var(--track));text-transform:var(--caps);padding:11px 12px;cursor:pointer;text-align:left}
.ctl .sh:hover{color:var(--ink)}
.ctl .sh .tri{color:var(--faint);font-size:10px;transition:transform .18s ease;width:9px;flex:0 0 auto}
.ctl .sec.open .sh .tri{transform:rotate(90deg);color:var(--accent)}
.ctl .sh .name{flex:1}
.ctl .sh .cnt{color:var(--faint);font-variant-numeric:tabular-nums;letter-spacing:calc(.04em * var(--track));text-transform:none}
.ctl .sh .cnt b{color:var(--accent);font-weight:600}
.ctl .sec .body{display:grid;grid-template-rows:0fr;transition:grid-template-rows .2s ease}
.ctl .sec.open .body{grid-template-rows:1fr}
.ctl .sec .body>div{overflow:hidden;min-height:0}
/* ── THE TAG AUTOCOMPLETE MUST BE ABLE TO ESCAPE THE SECTION ──────────────────
   Owner report 2026-07-29: "tag box gets cut off when searching for suggested tags".
   `.ac` is position:absolute and drops BELOW the input, but it sat inside a disclosure whose
   fold is the grid 0fr->1fr trick — and that trick REQUIRES overflow:hidden on the row box to
   clip while collapsed. So the dropdown was clipped at the section's edge and you could only
   read the first suggestion.
   The overflow is only load-bearing WHILE FOLDING, so it is relaxed once the section is open.
   The delay is the whole point: `transition:overflow 0s .2s` flips this discrete property
   AFTER the .2s fold, so content cannot spill out mid-expand. Closing has no transition on the
   base rule above, so it snaps back to hidden immediately and still clips as it collapses. */
.ctl .sec.open{overflow:visible;transition:overflow 0s .2s}
.ctl .sec.open .body>div{overflow:visible;transition:overflow 0s .2s}
.ctl .sec .inner{padding:2px 12px 13px}
.ctl .kv{font-size:12px;color:var(--dim);display:flex;flex-direction:column;gap:4px}
.ctl .kv b{color:var(--ink);font-weight:600}

/* similar posts (content discovery v1) — a compact grid of the shared .tile
   card, sized for the narrow ctl sidebar rather than the full masonry grid. */
.simgrid{display:grid;grid-template-columns:repeat(auto-fill,minmax(84px,1fr));gap:6px}
/* ⚖ THE STRIP'S WAY INTO THE SEARCH (forage-8mj). Deliberately the `famhead` shape one section
   up — a label on the left, one action on the right — because it is the same kind of row on the
   same sidebar, and a second layout for it would be a second thing to keep in step. It sits in
   `.inner`, never in `.sh`: the disclosure header IS a <button>. `.gen-open` is written for a
   <button>, so an <a> wearing it needs the two things a button gets for nothing. */
.simhead{display:flex;align-items:center;justify-content:space-between;gap:10px;margin-bottom:6px}
.simhead .famroot{font-size:11px;color:var(--faint)}
a.gen-open{display:inline-block;text-decoration:none}
.collgrid{grid-template-columns:repeat(auto-fill,minmax(140px,1fr));margin-top:12px}
.collcell{position:relative}
.collcell-rm{position:absolute;top:6px;right:6px;font-size:9px;padding:3px 7px;
  background:var(--panel);border-color:var(--hair-hi)}

/* tags — compact chips w/ inline ▲net▼ (mockup shape) */
.ctl .tagzones .tz{margin:0}
.ctl .tagzones .tz.suggested{margin-top:9px} .ctl .tagzones .tz.addzone{margin-top:9px}
.ctl .taglist{display:flex;flex-wrap:wrap;gap:6px}
.ctl .tag{display:inline-flex;align-items:center;gap:5px;border:1px solid var(--hair-hi);
  background:var(--raised);padding:3px 6px 3px 8px;font-size:12.5px;color:var(--accent);letter-spacing:0}
/* inherit, NOT --ink: this is (0,3,0) and would beat any kind rule outright, which is
   exactly why the post page's tag names ignored their kind colour entirely. */
.ctl .tag .nm{color:inherit}
/* border-COLOUR deliberately not set: it is the kind's channel (see the provenance block at
   the end of this file). At (0,3,0) this rule outranks that one, so naming a colour here
   silently greyed every AI chip on the post page. */
.ctl .tag.ai{border-style:dashed}
/* the dotted underline still marks an AI tag; the COLOUR is the kind's to set. */
.ctl .tag.ai .aidot{color:var(--faint)}
.ctl .tag.community{border-style:solid;border-color:var(--hair-hi)}
.ctl .tag.community .nm{color:#c98ce0}
.ctl .tag .tnet{color:var(--dim);font-size:11px;font-variant-numeric:tabular-nums}
.ctl .tag .ok{color:var(--faint);cursor:pointer;padding:0 1px} .ctl .tag .ok:hover{color:var(--good)}
.ctl .tag .x{color:var(--faint);cursor:pointer;padding:0 1px} .ctl .tag .x:hover{color:var(--bad)}
.ctl .tag .tv{display:inline-flex;align-items:center;gap:2px;border-left:1px solid var(--hair-hi);padding-left:5px;margin-left:2px}
.ctl .tag .tv button{background:none;border:0;color:var(--faint);cursor:pointer;font-size:11px;padding:0 1px;line-height:1}
.ctl .tag .tv button:hover{color:var(--ink)}
.ctl .tag .tv button.yes.on{color:var(--good)} .ctl .tag .tv button.no.on{color:var(--bad)}
.ctl .tag .verdicts{display:inline-flex;gap:2px;margin-left:2px}
.ctl .tag .verdicts button{background:none;border:0;color:var(--faint);cursor:pointer;font-size:11px;padding:0 1px}
.ctl .tag .verdicts button.reinforce:hover{color:var(--good)}
.ctl .tag .verdicts button.disapprove:hover{color:var(--bad)}
.ctl .tag .verdicts button.punish:hover{color:var(--accent)}
.ctl .confirmall{margin-top:9px}
.ctl .tz-h{font-size:9px;letter-spacing:calc(.2em * var(--track));text-transform:var(--caps);color:var(--faint);margin-bottom:6px}
.ctl .tz-h::before{content:"◈ ";color:var(--accent-deep)}
.ctl .sugrow{display:flex;align-items:center;gap:8px;flex-wrap:wrap;padding:4px 0;font-size:12px}
.ctl .sugrow .sugname{color:var(--ink)}
.ctl .sugrow .sugnet{color:var(--dim);font-variant-numeric:tabular-nums}
.ctl .sugrow .sugby{color:var(--faint);font-size:11px}
.ctl .sugrow .votes button{background:none;border:1px solid var(--hair-hi);color:var(--faint);cursor:pointer;font-size:11px;padding:1px 5px}
.ctl .sugrow .votes button.yes.on{color:var(--good);border-color:var(--good)}
.ctl .sugrow .votes button.no.on{color:var(--bad);border-color:var(--bad)}
.ctl .addtag{display:flex;margin-top:0;border:1px solid var(--hair-hi);background:var(--sunk);position:relative}
.ctl .addtag input{flex:1;background:none;border:0;font-size:12.5px;padding:6px 9px}
.ctl .addtag input:focus{border:0;outline:none}
.ctl .addtag .tagadd-go{flex:0 0 auto;background:var(--raised);border:0;border-left:1px solid var(--hair-hi);
  color:var(--accent);width:34px;cursor:pointer;font-size:15px;padding:0}
.ctl .addtag .tagadd-go:hover{background:var(--hair);color:var(--bright)}

/* family (folded genealogy) */
.ctl .famroot{font-size:11px;color:var(--faint);margin-bottom:6px}
.ctl .famnode{display:flex;align-items:center;gap:8px;padding:4px 0}
.ctl .famnode .thumb{width:40px;height:40px;flex:none;border:1px solid var(--hair);object-fit:cover;background:var(--sunk)}
.ctl .famnode.cur{outline:1px solid var(--accent);outline-offset:1px}
.ctl .famnode a{color:var(--ink)}
.ctl .famkids{margin-left:12px;padding-left:12px;border-left:2px solid var(--hair-hi)}

/* comments (folded) */
.ctl .comments-body .commentlist{display:flex;flex-direction:column;gap:12px}
.ctl .comments-body .cformbox{margin-top:12px}
.ctl .comments-body .cform{display:flex;gap:6px;align-items:flex-start;border:1px solid var(--hair-hi);background:var(--sunk)}
.ctl .comments-body .cinput{flex:1 1 auto;min-width:0;resize:vertical;font-size:12.5px;border:0;background:none;padding:8px 10px}
.ctl .comments-body .cinput:focus{outline:none;border:0}
.ctl .comments-body .cbtn.send{flex:0 0 auto;border:0;border-left:1px solid var(--hair-hi);background:var(--raised);
  color:var(--accent);width:36px;align-self:stretch;cursor:pointer;font-size:16px}
.ctl .comments-body .cbtn.send:hover{color:var(--bright);background:var(--hair)}

/* provenance (folded) */
.ctl .prov{display:flex;flex-direction:column;gap:6px;font-size:12px}
.ctl .prov .r{display:flex;gap:10px}
.ctl .prov .k{color:var(--faint);width:64px;flex:0 0 auto;text-transform:var(--caps);letter-spacing:calc(.06em * var(--track));font-size:10px;padding-top:1px}
.ctl .prov .v{color:var(--ink)}
.ctl .prov a{color:var(--accent)}

@media(prefers-reduced-motion:reduce){
  .ctl .sec .body{transition:none}
  .ctl .sh .tri{transition:none}
}
@media(max-width:820px){
  .post{gap:14px}
  .actionbar{gap:6px}
}

/* =====================================================================
   THE MEME GALAXY (content web phase 1) — a full-bleed canvas starmap.
   The canvas is transparent so the faint rotating shred spiral (.shredbg)
   and the void show through; galaxymap.js draws jewel-glow stars over it.
   The stage is pinned below the masthead (--rail-clear = the JS-measured
   masthead height, set in renderHeader) to the viewport bottom, so it fills
   the screen at any header wrap without a hardcoded height.
   ===================================================================== */
.galaxy-wrap{margin:0}
.galaxy-stage{position:fixed;left:0;right:0;bottom:0;top:var(--rail-clear,64px);
  overflow:hidden;touch-action:none;user-select:none;-webkit-user-select:none}
.galaxy-canvas{position:absolute;inset:0;display:block;touch-action:none;cursor:grab}
.galaxy-canvas:active{cursor:grabbing}

/* HUD: a quiet top-left readout + a recenter control, terminal-jewel styling. */
.galaxy-hud{position:absolute;top:12px;left:14px;z-index:2;display:flex;align-items:center;
  gap:10px;flex-wrap:wrap;pointer-events:none}
.galaxy-hud-line{font-size:10.5px;letter-spacing:calc(.14em * var(--track));text-transform:var(--caps);
  color:var(--dim);background:rgba(10,11,13,.5);border:1px solid var(--hair);
  padding:4px 9px;backdrop-filter:blur(2px)}
.galaxy-hud-btn{pointer-events:auto;border:1px solid var(--hair-hi);color:var(--accent);
  background:rgba(10,11,13,.55);padding:4px 10px;letter-spacing:calc(.12em * var(--track));text-transform:var(--caps);
  font-size:10px;cursor:pointer;backdrop-filter:blur(2px)}
.galaxy-hud-btn:hover{color:var(--bright);border-color:var(--accent)}

/* the hover card: the star's thumb + its title (title via textContent — XSS law). */
.galaxy-card{position:absolute;z-index:3;width:150px;pointer-events:none;
  background:rgba(12,14,18,.94);border:1px solid var(--hair-hi);
  box-shadow:0 6px 24px rgba(0,0,0,.55),0 0 18px rgba(237,167,51,.12);padding:6px}
.galaxy-card-thumb{display:block;width:100%;height:110px;object-fit:cover;background:var(--sunk)}
.galaxy-card-cap{margin-top:5px;font-size:11px;line-height:1.35;color:var(--ink);
  max-height:2.7em;overflow:hidden;text-overflow:ellipsis;
  display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical}

/* the still-forming / error notice, centered in the field. */
.galaxy-empty{position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);
  max-width:420px;text-align:center;color:var(--dim);font-size:12.5px;line-height:1.6;
  letter-spacing:calc(.02em * var(--track));padding:0 20px}

/* ---- the meme galaxy TAG LAYER (content web phase 2) ---- */
.galaxy-hud{align-items:flex-start}
.galaxy-tagctl{position:relative;pointer-events:auto}
.galaxy-tagin{width:150px;font-size:11px;padding:4px 8px;background:rgba(10,11,13,.6);
  border:1px solid var(--hair-hi);color:var(--ink);letter-spacing:calc(.06em * var(--track));
  backdrop-filter:blur(2px)}
.galaxy-tagin::placeholder{color:var(--faint);letter-spacing:calc(.1em * var(--track));text-transform:var(--caps);font-size:10px}
.galaxy-tagin:focus{border-color:var(--accent);outline:none}
.galaxy-sugg{position:absolute;top:calc(100% + 3px);left:0;min-width:150px;max-height:220px;
  overflow-y:auto;z-index:5;background:rgba(12,14,18,.97);border:1px solid var(--hair-hi);
  box-shadow:0 8px 24px rgba(0,0,0,.6)}
.galaxy-sugg-item{display:flex;justify-content:space-between;gap:10px;padding:5px 9px;
  font-size:11px;color:var(--ink);cursor:pointer;letter-spacing:calc(.04em * var(--track))}
.galaxy-sugg-item:hover,.galaxy-sugg-item.on{background:var(--raised);color:var(--bright)}
.galaxy-sugg-c{color:var(--faint);font-variant-numeric:tabular-nums}

.galaxy-legend{display:flex;gap:6px;flex-wrap:wrap;pointer-events:auto;max-width:60vw}
.galaxy-chip{display:inline-flex;align-items:center;gap:6px;padding:3px 6px 3px 7px;
  background:rgba(10,11,13,.6);border:1px solid var(--hair-hi);font-size:11px;
  color:var(--ink);letter-spacing:calc(.03em * var(--track));backdrop-filter:blur(2px)}
.galaxy-chip-sw{width:9px;height:9px;border-radius:50%;box-shadow:0 0 6px currentColor;flex:0 0 auto}
.galaxy-chip-nm{white-space:nowrap;max-width:160px;overflow:hidden;text-overflow:ellipsis}
.galaxy-chip-x{background:none;border:none;color:var(--dim);cursor:pointer;font-size:14px;
  line-height:1;padding:0 0 0 2px}
.galaxy-chip-x:hover{color:var(--accent)}

/* ============================================================
   QUEUE view v2 (docs/SPEC_queue.md) — iFunny-style triage. One post in full
   focus + a fixed thumb-reachable verdict bar. Reuses the existing .tag / .comment
   / .vote surfaces; only the focus layout + verdict bar are new. Mobile-first:
   big targets, no hover-only affordances. ============================================================ */
.queuewrap{max-width:760px;padding-bottom:132px}   /* clear the fixed verdict bar */
.qtop{display:flex;align-items:center;justify-content:space-between;gap:10px;
  padding:8px 0 6px;border-bottom:1px solid var(--hair);margin-bottom:12px}
/* ═══ ONE ANATOMY FOR "GO BACK ONE LEVEL" (forage-qw2w.34) ═══════════════════════════════
   Owner review 2026-09-05: the same act is *"a FULL-WIDTH bordered box, centred `← OVERVIEW`"*
   on /admin/* and *"bare grey text, no box, no border, floating on the page ground"* on /data.
   ⚠ RE-MEASURED ON MASTER AT 390x844 BEFORE TOUCHING IT, because the bead is three days old and
   its headline number had already moved: the admin box is 336x44 now, not the 90px tall pair of
   halves the review photographed (`.as-acts` lost its second half to the burger). What did NOT
   move is the anatomy — 336x44 bordered against 49x17 unbordered, the same word, the same 11px
   caps, one drawn as a control and one as prose.
   It is a HAIRLINE BOX now: the resting shape `.as-back` wears (app-admin.css) and the shape
   every other control in this design wears.

   ⚠ THE WIDTH IS THE ROW'S, NOT THE CONTROL'S, and that is the same rule the admin suite
   already applies to itself — `.as-back` measures 336x44 at 390 (a grid row of its own) and
   101x29 at 1440 (an inline chip in a flex strip). This one shares `.qtop` with the progress
   readout, so it takes its own width. Same anatomy, different row.
   ⚠ AND IT IS `.qback`, NOT `.pns-data-back`, BECAUSE THREE SURFACES DRAW IT. page-data.js and
   deck-shared.js (twice) all build `el("a", "qback", "‹ grid")`, so /data /queue /collective
   every one carried the bare-text shape. Painting only the screen the bead photographs would
   have left the identical two anatomies one tap apart — the DUP failure in as few words as it
   can be made.
   ⚠ 26px DRAWN, 44px TARGETED, and here the deck is the reason rather than taste: `html.datadeck`
   DOES NOT SCROLL (tagpoll.css), so every pixel spent on the head is a pixel off the stage the
   card has to fit in. The box costs 9px of drawn height and `.pns-data-top` gives 6 of them back,
   so /data's head moves 29px -> 32px. ⛔ Do not grow it to a drawn 44: that is 15px off a card
   that already has none to give, which is what the app-screens.css note beside `.pns-data-top`
   has said since forage-qw2w.18.1.
   ⚑ THE HALO IS THE ONE app-screens.css USED TO DECLARE FOR /data ALONE. It covers all three
   deck surfaces from here — /queue and /collective had no floor at all before this, at 49x17. */
.qback{display:inline-flex;align-items:center;position:relative;min-height:26px;
  padding:0 10px;border:1px solid var(--hair);
  font-size:11px;letter-spacing:calc(.1em * var(--track));text-transform:var(--caps);color:var(--dim)}
.qback:hover{color:var(--accent);border-color:var(--hair-hi)}
/* THE 44px FLOOR AS AN OVERHANG (docs/archive/DECISIONS.md "The floor may not change what a
   member can see"): vertical only, because a horizontal halo would overhang whatever shares
   the row and steal its taps. mobiletaps.test.js credits this shape to `.qback`. */
@media(pointer:coarse){
  .qback::after{content:"";position:absolute;left:0;right:0;top:50%;
    transform:translateY(-50%);height:var(--tap-min)}
}
.qprogress{font-size:11px;letter-spacing:calc(.06em * var(--track));text-transform:var(--caps);color:var(--faint);
  font-variant-numeric:tabular-nums}
.qprogress .qfresh{color:var(--accent)}
.qprogress .qbacklog{color:var(--dim)}
.qloading{color:var(--faint);padding:40px 0}

.qstage{position:relative;background:var(--sunk);border:1px solid var(--hair);
  display:flex;align-items:center;justify-content:center;overflow:hidden;min-height:120px}
.qstage .qimg,.qstage .qvideo{max-height:70vh;width:auto;margin:0 auto;object-fit:contain}
.qtiled{width:100%;margin:0 auto;background:var(--sunk)}
.qtiled img{display:block;width:100%}
/* NSFW/NSFL veil (shared shape with post.html — reuses .nsfw-veil/.nvbox). */
.qstage.nsfw-veiled>.qimg,.qstage.nsfw-veiled>.qvideo,.qstage.nsfw-veiled>.qtiled{
  filter:blur(38px);transform:scale(1.03)}

.qmeta{margin:12px 0 4px}
.qtitle{color:var(--bright);font-size:15px;line-height:1.35;margin-bottom:4px}
.qby{font-size:12px;color:var(--dim)}
.qby .qage{color:var(--faint)}
.qby .qopen{font-size:11px;text-transform:var(--caps);letter-spacing:calc(.08em * var(--track))}
/* FAVORITES: compact private save toggle on the queue card's action row. Matches the
   .qopen link idiom; saved state = accent star. */
.qby .qfav{background:none;border:0;cursor:pointer;color:var(--dim);font-size:11px;
  text-transform:var(--caps);letter-spacing:calc(.08em * var(--track));padding:0;font-family:inherit}
.qby .qfav:hover{color:var(--ink)}
.qby .qfav.on{color:var(--accent)}

.qtags{margin:10px 0 4px}
.qtags .taglist{display:flex;flex-wrap:wrap;gap:6px}

.qsec-h{font-size:10px;letter-spacing:calc(.16em * var(--track));text-transform:var(--caps);color:var(--dim);
  margin:20px 0 10px;padding-top:14px;border-top:1px solid var(--hair)}
.qcomments{margin-top:6px}

/* the fixed verdict bar — pinned bottom, thumb-sized targets, always reachable. */
.verdictbar{position:fixed;left:0;right:0;bottom:0;z-index:var(--z-bar);
  background:var(--panel);border-top:1px solid var(--hair-hi);
  /* ⚠ max(), NOT `8px + env()`. Under viewport-fit=cover (queue.html:7) the layout viewport
     extends UNDER the Android navigation bar, so env(safe-area-inset-bottom) is already the
     clearance this bar needs — around 34-48px on the owner's phone. Adding our own 8px on top
     of it was a double-count, and it is what put a band of dead bar under the verdict glyphs
     (owner 2026-08-15, measured off the screenshot: 96 device px of pure black below the last
     pixel of content). The inset is the floor; 8px is what we use when there is no inset. */
  padding:8px max(14px,env(safe-area-inset-left)) max(8px,env(safe-area-inset-bottom,0px));
  box-shadow:0 -6px 20px -8px rgba(0,0,0,.6)}
.qbarmsg{margin:0 auto 6px;max-width:760px;text-align:center;min-height:0}
.qbarmsg.bad{color:var(--bad)}
.verdictrow{max-width:760px;margin:0 auto;display:flex;gap:8px;align-items:stretch}
/* the verdict control REUSES the post-vote .vote/.vbtn idiom (▼ – ▲, one .on),
   enlarged to thumb-sized targets and segmented into one bordered box. */
.verdictvote{flex:1 1 auto;justify-content:stretch;gap:0;padding:0}
.verdictvote .vbtn{flex:1 1 0;min-height:56px;font-size:22px;padding:6px 10px;
  display:flex;align-items:center;justify-content:center;transition:color .12s,background .12s}
.verdictvote .vbtn + .vbtn{border-left:1px solid var(--hair)}
.verdictvote .vbtn:not(:disabled):active{background:var(--raised)}
.verdictvote .vbtn.neutral.on{color:var(--accent)}   /* up.on/down.on already good/bad */
.verdict-next{min-height:56px;min-width:88px;flex:0 0 auto;letter-spacing:calc(.08em * var(--track));
  text-transform:var(--caps);font-size:12px}
.verdict-next:not(:disabled):hover{border-color:var(--accent);color:var(--accent)}
/* PREVIOUS (owner ruling 2026-07-22m) — a quieter secondary control, mirrors
   verdict-next's shape but smaller/dimmer so NEXT stays the visually primary
   action. Disabled (nothing on HISTORY yet) fades further, no hover affordance. */
.verdict-prev{min-height:56px;min-width:64px;flex:0 0 auto;letter-spacing:calc(.06em * var(--track));
  text-transform:var(--caps);font-size:11px;color:var(--faint)}
.verdict-prev:not(:disabled):hover{border-color:var(--accent);color:var(--accent)}
.verdict-prev:disabled{opacity:.35;cursor:default}

/* caught-up screen */
.qcaught{text-align:center;padding:64px 16px}
.qcaught-mark{font-size:40px;color:var(--good);line-height:1;margin-bottom:14px}
.qcaught-h{font-size:18px;color:var(--bright);letter-spacing:calc(.02em * var(--track));margin-bottom:6px}
.qcaught-sub{margin:0 auto 22px;max-width:420px}
.qcaught-acts{display:flex;gap:10px;justify-content:center;flex-wrap:wrap}
.qcaught-stats{margin:0 auto 20px;max-width:460px;font-size:12px;letter-spacing:calc(.06em * var(--track));
  color:var(--dim);font-variant-numeric:tabular-nums}

/* ============================================================
   JUICE v2 (docs/SPEC_juice_v2.md) — combo meter, minigames section, recap panel,
   award-unlock stamp. All cosmetic; NEVER implies score.
   ============================================================ */
/* ARCADE meter (owner ruling 2026-07-19d). The MULTIPLIER floats over the NEXT
   button (the thumb's cash-in spot); the run's queue score lives in the status
   line. The badge NEVER intercepts taps (pointer-events:none). */
.verdict-nextwrap{position:relative;flex:0 0 auto;display:flex}
.multbadge{position:absolute;left:50%;top:-9px;transform:translate(-50%,-100%);pointer-events:none;
  white-space:nowrap;font-size:13px;font-weight:700;font-variant-numeric:tabular-nums;line-height:1;
  padding:3px 8px;border-radius:999px;border:1px solid var(--hair);background:var(--panel);
  color:var(--faint);opacity:0;transition:opacity .18s,color .15s,border-color .15s,box-shadow .2s;
  overflow:hidden}
.multbadge.on{opacity:1;color:var(--mult-ink);
  border-color:rgba(255,217,138,calc(.3 + .5*var(--heat,0)));
  box-shadow:0 0 calc(6px + 18px*var(--heat,0)) rgba(255,180,90,calc(.14 + .34*var(--heat,0)))}
.multbadge.pop{animation:mult-pop .24s ease-out}
.mb-text{position:relative}
/* THIN countdown (owner ruling 2026-07-22l): the run's 10-minute chain window,
   draining left->right via a plain CSS width transition (armCountdown, common.js)
   — no setInterval polling. Sits along the pill's bottom inside edge; the
   badge's own overflow:hidden clips its corners to the rounded shape. */
.mb-fill{position:absolute;left:0;bottom:0;height:2px;width:100%;
  background:rgba(255,217,138,.55)}
@keyframes mult-pop{0%{transform:translate(-50%,-100%) scale(1)}
  45%{transform:translate(-50%,-100%) scale(1.32)}100%{transform:translate(-50%,-100%) scale(1)}}
/* the run score in the bar status line */
.qbarstatus{max-width:760px;margin:5px auto 0;text-align:center}
/* the run score gets the SAME escalating family as the ×badge (owner ruling 19e):
   warmth/glow scales with --heat (cool -> warm ~5 -> hot ~15 -> blazing 30+), and
   it POPS on every bank (--pop scaled so a big cash-in pops harder). inline-block so
   the pop transform applies; no layout shift (transform only). */
/* ⚠ THE NEGATIVE MARGIN CANCELS THE TRAILING LETTER-SPACE (owner report 2026-08-18,
   forage-8le: "the text inside the score pill is not centered"). letter-spacing adds its
   space AFTER every glyph including the last, so a centred readout's ink sits half that
   space left of true centre — in the site-wide HUD pill (common-points.js mounts this same
   element inside .arcade-hud-pill) it reads as lopsided padding. Cancel exactly what the
   tracking added; a skin that sets --track:0 cancels nothing. */
.qscore{display:inline-block;font-size:11px;letter-spacing:calc(.06em * var(--track));
  margin-right:calc(-.06em * var(--track));color:var(--dim);
  font-variant-numeric:tabular-nums;transition:color .2s,text-shadow .2s}
.qscore.on{color:color-mix(in srgb, var(--dim), #ff8a2a calc(var(--heat,0)*100%));
  text-shadow:0 0 calc(3px + 18px*var(--heat,0)) rgba(255,150,70,calc(.12 + .5*var(--heat,0)))}
.qscore.pop{animation:qscore-pop .26s ease-out}
@keyframes qscore-pop{0%{transform:scale(1)}45%{transform:scale(var(--pop,1.3))}100%{transform:scale(1)}}
@media(prefers-reduced-motion:reduce){.qscore.pop{animation:none}}
/* the "new best" burst, floated above the multiplier badge */
.combo-best{position:absolute;left:50%;top:-8px;transform:translate(-50%,-100%);white-space:nowrap;
  font-size:10px;letter-spacing:calc(.14em * var(--track));text-transform:var(--caps);font-weight:700;color:#0a0b0d;
  background:linear-gradient(100deg,#ffe9a8,#ffd98a,#ffe9a8);padding:4px 9px;border-radius:3px;
  box-shadow:0 0 18px rgba(255,217,138,.5);animation:combo-best-in .3s ease-out}
@keyframes combo-best-in{from{opacity:0;transform:translate(-50%,-70%) scale(.7)}
  to{opacity:1;transform:translate(-50%,-100%) scale(1)}}
@media(prefers-reduced-motion:reduce){.multbadge.pop{animation:none}.combo-best{animation:none}}

/* ═══ THE SAME BADGE, STOOD UP IN A COLUMN (the masthead arcade column, 2026-08-26) ═══════════
   Everything above assumes the badge FLOATS: absolute, left:50%, top:-9px, translated clear
   ABOVE its box, because on the verdict bar it hangs over the NEXT button the thumb cashes in
   on. The masthead has nothing above it but the top of the screen, so a floated badge there is
   drawn off the page. `mbcol` is the same badge in the flow instead — every colour, the heat
   glow, the pop and the countdown fill are unchanged; only the GEOMETRY moves.
   ⚠ `position:static`, AND THAT IS DELIBERATE RATHER THAN CARELESS. `.mb-fill` is absolutely
   positioned, so making this box un-positioned hands it to `.arcadecol` (app-rank.css) — which
   is exactly what turns the countdown into the column's third LINE, a 2px rule along its foot,
   instead of a hairline hiding under a 10px multiplier. The badge's own `overflow:hidden` does
   not clip it: the fill's containing block is now an ANCESTOR of this element.
   ⚠ AND THE BOX GOES. The pill's border, background and radius exist to separate it from a
   photograph it is floating over; in a bar it would be a box inside a box, and the owner's
   sketch is plain stacked numbers. `.on` still does all the lighting.
   ⚠ OPAQUE AT ×1.0 — the floating badge fades OUT below ×1.1 because empty air over a button is
   invisible, but this column's other two lines are already drawn and a missing top line reads as
   a broken readout, not as a quiet one. */
.multbadge.mbcol{position:static;transform:none;opacity:1;font-size:10px;font-weight:600;
  padding:0;border:none;background:none;border-radius:0;overflow:visible}
.multbadge.mbcol.pop{animation:mult-pop-flat .24s ease-out}
@keyframes mult-pop-flat{0%{transform:scale(1)}45%{transform:scale(1.32)}100%{transform:scale(1)}}
/* the run score, given the line the sketch gives it — and given it as a PLAIN NUMBER. `.qs-l`
   is MgMeter's own "score " label (scoreFace, common-audio.js), which the deck bar's status line
   needs and this column does not: the ×N.N above it and the countdown below it already say what
   the number is, and measured on the real bar at 390px the word was 64 of the column's 98px —
   the difference between a slot that fits beside the brand and one that squeezes it. */
.arcadecol .qscore{font-size:14px;line-height:1.15}
.arcadecol .qs-l{display:none}
/* ⚠ AND THE COOL END OF THE HEAT SCALE IS `--bright` HERE, NOT `--dim`. Same formula as
   `.qscore.on` above, same #ff8a2a, same --heat — only the colour it warms FROM changes. The
   meter's dim base was chosen for a status line floating over a photograph; on this bar the
   number sits between two `--bright` numbers of exactly its own size, and a grey one there does
   not read as "cool", it reads as disabled. ⛔ Do not re-tune the heat stops in this rule: they
   belong to the meter, and a second copy of them drifts. */
.arcadecol .qscore{color:var(--bright)}
.arcadecol .qscore.on{color:color-mix(in srgb, var(--bright), #ff8a2a calc(var(--heat,0)*100%))}
/* the "new best" burst drops DOWN out of the bar, for the same reason the badge stopped
   floating: above the masthead is off-screen, and a celebration nobody sees is a wasted one. */
.multbadge.mbcol .combo-best{top:auto;bottom:-8px;transform:translate(-50%,100%);
  animation:combo-best-down .3s ease-out}
@keyframes combo-best-down{from{opacity:0;transform:translate(-50%,70%) scale(.7)}
  to{opacity:1;transform:translate(-50%,100%) scale(1)}}
@media(prefers-reduced-motion:reduce){.multbadge.mbcol.pop{animation:none}
  .multbadge.mbcol .combo-best{animation:none}}

/* minigames profile section — records + dynamic apex badge (gold, matches ASCENT). */
.mg-records{display:flex;flex-direction:column;gap:6px;max-width:520px}
.mg-row{display:flex;align-items:baseline;gap:10px;padding:5px 2px;border-bottom:1px solid var(--hair)}
.mg-k{flex:0 0 auto;min-width:118px;font-size:11px;letter-spacing:calc(.1em * var(--track));text-transform:var(--caps);color:var(--dim)}
.mg-v{font-size:17px;font-weight:700;color:var(--bright);font-variant-numeric:tabular-nums}
.mg-when{font-size:10.5px;color:var(--faint)}
/* minigame apex badge — PRISMATIC, NO fill (owner ruling 2026-07-20i): clipped-cycling text
   + prismchip cycling border, normal panel behind. */
.mg-apex{margin-left:auto;font-size:9.5px;letter-spacing:calc(.2em * var(--track));text-transform:var(--caps);font-weight:700;
  padding:2px 8px;border-radius:999px;border:1px solid #eafff0;
  background:var(--prism);background-attachment:scroll;background-size:var(--prism-span);background-position:var(--sheet-pos,0 0);
  -webkit-background-clip:text;background-clip:text;color:transparent;
  animation:prismchip 6s linear infinite}
.mg-note{margin-top:8px;font-size:10.5px}
@media(prefers-reduced-motion:reduce){.mg-apex{animation:none}}

/* login recap panel — a dismissible terminal card at the top of the grid. */
.recap-panel{margin:0 0 14px;border:1px solid var(--hair-hi);border-left:3px solid var(--accent);
  background:var(--panel);border-radius:3px;padding:12px 14px;box-shadow:0 6px 22px -10px rgba(0,0,0,.6);
  animation:recap-in .28s ease-out}
.recap-head{display:flex;align-items:center;justify-content:space-between;margin-bottom:8px}
.recap-title{font-size:10px;letter-spacing:calc(.22em * var(--track));text-transform:var(--caps);color:var(--accent)}
.recap-x{position:relative;background:none;border:none;color:var(--dim);font-size:20px;line-height:1;
  cursor:pointer;min-width:32px;min-height:32px;padding:0}
.recap-x:hover{color:var(--bright)}
/* ⚖ 32px DRAWN, 44px TARGETED (forage-num, 2026-08-21). MEASURED 32x32 in the real app at
   390x844 — the recap panel is the first thing a member sees on the grid after a day away and
   its only dismiss is this ×. A `min-height:var(--tap-min)` here would grow `.recap-head`, and
   the head is a flex row whose height is the panel's top edge: that is a change to what a
   member SEES, which the floor may not spend (docs/DECISIONS.md "The floor may not change what
   a member can see"). So it takes the CORNER HALO (`.pin-unpin::after`, app.css) rather than
   the vertical overhang `.btn` takes: there is nothing beside it to steal a tap from — the
   panel's own 14px padding is to its right and `.recap-title` MEASURED 123px to its left. */
@media(pointer:coarse){
  .recap-x::after{content:"";position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);
    width:var(--tap-min);height:var(--tap-min)}
}
.recap-lines{display:flex;flex-direction:column;gap:4px}
.recap-line{font-size:13px;color:var(--ink)}
.recap-line.dim{color:var(--dim)}
.recap-link{color:var(--accent)}
@keyframes recap-in{from{opacity:0;transform:translateY(-8px)}to{opacity:1;transform:none}}
.recap-panel.recap-out{opacity:0;transform:translateY(-8px);transition:opacity .2s,transform .2s}
@media(prefers-reduced-motion:reduce){.recap-panel{animation:none}.recap-panel.recap-out{transition:none}}

/* award-unlock ceremony — the medal STAMPS in with a rarity burst ring. */
.award-badge.aw-unlock{animation:aw-stamp .5s cubic-bezier(.2,1.4,.5,1) both}
@keyframes aw-stamp{0%{opacity:0;transform:scale(2.2) rotate(-8deg)}
  60%{opacity:1;transform:scale(.92) rotate(2deg)}100%{transform:scale(1) rotate(0)}}
.aw-burst{position:absolute;inset:0;border-radius:inherit;pointer-events:none;
  box-shadow:0 0 0 2px currentColor;opacity:.9;animation:aw-burst-ring .7s ease-out forwards}
@keyframes aw-burst-ring{0%{transform:scale(.6);opacity:.9}100%{transform:scale(1.9);opacity:0}}
@media(prefers-reduced-motion:reduce){.award-badge.aw-unlock{animation:none}.aw-burst{display:none}}

/* ============================================================
   COLLECTIVE (docs/SPEC_collective.md) — reuses the QUEUE full-focus layout +
   the verdict bar's .vote/.vbtn idiom. Only the per-post status line, the mod-vote
   net box, and the locked screen are collective-specific. ============================================================ */
.cstatus{max-width:760px;margin:0 auto 10px;font-size:11px;letter-spacing:calc(.04em * var(--track));
  text-transform:var(--caps);color:var(--faint);font-variant-numeric:tabular-nums;
  display:flex;flex-wrap:wrap;align-items:center;gap:2px}
.cstatus .cnet{color:var(--ink);font-weight:600}
.cstatus .cgap{color:var(--dim)}
.cstatus .cexpiry{color:var(--accent)}
/* the mod-vote box: ▼ net ▲ (approve=up/green, reject=down/red), hairlines around
   the net readout so the three segments read as one bordered control. */
.verdictvote.modvote .vnet{flex:0 0 auto;display:flex;align-items:center;justify-content:center;
  min-width:3.2em;font-size:16px;color:var(--ink);border-left:1px solid var(--hair);border-right:1px solid var(--hair)}
.qlocked .qcaught-mark{color:var(--dim)}

/* ============================================================
   HOT tab (docs/SPEC_hot.md) — active discussions, BOARD-INDEX layout (owner ruling
   2026-07-19, 4chan board-index review: "images far left, comments get a TON of
   room"). FULL-PAGE-WIDTH rows separated by plain hairline rules (no boxed card, no
   max-width column). Thumb pinned FAR LEFT (~250px, natural aspect, top-aligned);
   header line + the last 3 comments (full-text reply-cell blocks) flow in ALL the
   remaining width. The WHOLE row is one tap-target to the post's comments. Mobile
   stacks it, still full-bleed with rules. ============================================================ */
/* the hot page escapes .wrap's centered 1200px column: rows start at the TRUE
   left edge of the page (4chan board-index geometry). On rail-width screens the
   column stops short of the fixed rail (right:16px + 260px + gap). */
.hotwrap{max-width:none;margin:0;padding:0 18px}
@media(min-width:1800px){.hotwrap{padding-right:304px}}
.hotlist{display:flex;flex-direction:column}
.hotrow{display:flex;gap:20px;align-items:flex-start;padding:14px 2px;
  border-bottom:1px solid var(--hair);text-decoration:none;color:inherit}
.hotrow:hover{background:rgba(255,255,255,.016)}
/* thumb: far left, small (board-index scale) — natural aspect, top-aligned;
   the CAP keeps a tall poster from towering over a one-comment row */
.hotthumb{position:relative;flex:0 0 160px;width:160px;background:var(--sunk);overflow:hidden}
.hotthumb img{display:block;width:100%;height:auto;max-height:200px;object-fit:contain;object-position:top left}
.hotthumb .badge{position:absolute;top:6px;right:6px;font-size:9px;letter-spacing:calc(.08em * var(--track));
  text-transform:var(--caps);padding:2px 6px;background:rgba(10,11,13,.82);
  border:1px solid var(--hair-hi);color:var(--accent)}
.hotthumb.veiled img{filter:blur(16px)}
.hotveil{position:absolute;inset:0;display:flex;align-items:center;justify-content:center;
  font-size:11px;letter-spacing:calc(.28em * var(--track));color:var(--bad);background:rgba(10,11,13,.35);pointer-events:none}
/* discussion: takes ALL remaining width, top-aligned beside the thumb */
.hotdisc{flex:1 1 auto;min-width:0;display:flex;flex-direction:column;gap:12px}
.hothead{font-size:13px;line-height:1.4;color:var(--dim);overflow-wrap:anywhere}
.hothead .hotname{color:var(--bright);font-weight:600}
.hothead .hotage{color:var(--dim);font-variant-numeric:tabular-nums}
/* ⛔ `.hotcount`/`.hotdot` gone — 3bb2fae0, one meta row: the count is a `.pn-loz`. */
/* comments: full-text reply-cell BLOCKS on the slightly-raised bg, air between */
.hotcomments{display:flex;flex-direction:column;align-items:flex-start;gap:10px}
/* reply cells SHRINK TO FIT their text (the 4chan cell signature) — never a
   full-width empty bar around a three-word comment */
.hotc{background:var(--raised);border:1px solid var(--hair);padding:9px 12px;
  max-width:100%;min-width:180px}
.hotc-a{display:block;font-size:10px;letter-spacing:calc(.06em * var(--track));text-transform:lowercase;
  color:var(--faint);margin-bottom:4px}
.hotc-b{font-size:12.5px;line-height:1.55;color:var(--ink);
  white-space:pre-wrap;overflow-wrap:anywhere;
  display:-webkit-box;-webkit-line-clamp:4;-webkit-box-orient:vertical;overflow:hidden}
/* mobile: stack — thumb full-width above, blocks beneath, still full-bleed + rules */
@media(max-width:640px){
  .hotrow{flex-direction:column;gap:12px}
  .hotthumb{flex:0 0 auto;width:100%}
  .hotthumb img{max-height:260px}
}
/* honest empty state */
.hotempty{text-align:center;padding:60px 16px}
.hotempty-mark{font-size:36px;color:var(--info);line-height:1;margin-bottom:12px}
.hotempty-h{font-size:18px;color:var(--bright);letter-spacing:calc(.02em * var(--track));margin-bottom:6px}
.hotempty-sub{margin:0 auto 20px;max-width:420px}
.hotempty-acts{display:flex;gap:10px;justify-content:center;flex-wrap:wrap}

/* ============================================================
   COMMENT QOL v1 (docs/SPEC_comment_qol.md) — greentext · >>refs · #tags ·
   thread rails + collapse · quote flash · double-tap heart. Render-time only;
   every token is a textContent span (XSS law). ============================================================ */
/* greentext: a muted meme-green that sits in the terminal palette */
.ctext .greentext{color:#8faa4e}
/* inline references + tag links */
.cref{cursor:pointer}
.cref.quote{color:var(--info);text-decoration:none}
.cref.quote:hover{text-decoration:underline}
.cref.ctag{color:var(--accent);text-decoration:none}
.cref.ctag:hover{text-decoration:underline}
.cref.cmention{color:var(--accent);font-weight:600;text-decoration:none}
.cref.cmention:hover{text-decoration:underline}
.cref.plain{color:var(--info);opacity:.8}                 /* HOT preview: styled, inert */
.cref.embed.loading{color:var(--info);opacity:.7}
.cref.embed.dead{color:var(--faint);text-decoration:line-through;cursor:default} /* 404/403 idiom */
.cref.embed.card{display:inline-flex;align-items:center;gap:4px;vertical-align:middle;
  border:1px solid var(--hair-hi);background:var(--sunk);padding:1px 5px 1px 1px;text-decoration:none;
  color:var(--info);line-height:1}
.cref.embed.card:hover{border-color:var(--accent-deep)}
.cref-thumb{width:22px;height:22px;object-fit:cover;display:block;background:var(--sunk)}
.cref-lbl{font-size:11px}
/* backlinks row (who quoted this comment) */
.cbacklinks{font-size:10.5px;color:var(--faint);margin-top:4px}
.cbacklinks .cbl-lbl{text-transform:var(--caps);letter-spacing:calc(.08em * var(--track))}
/* desktop hover preview of a quoted comment (a clone) */
.cquote-preview{position:absolute;z-index:var(--z-tooltip);max-width:340px;background:var(--panel);
  border:1px solid var(--hair-hi);padding:8px 10px;box-shadow:0 8px 24px -8px rgba(0,0,0,.7);
  font-size:12px;pointer-events:none}
.cquote-preview .cmeta{margin-bottom:4px}
/* ---- COMMENT EDITING (owner rulings R9 + R12, 2026-08-15; .scratch/feedback-forum/EDITING.md) ----
   ⚠ STYLED ON ITS OWN CLASS, NEVER AS `.cmeta .cedited`. `.cmeta` is NOT a comment-only class —
   web/page-dupes.js builds one for a DUPE-CANDIDATE card (#id, dimensions, age, "keeps this") — so
   a descendant selector would be scoped wider than the three comment surfaces this control lives
   on. The viewer sheet's metaline is `.vw-cmeta` anyway, so a `.cmeta` descendant rule would also
   have missed the phone, which is the primary surface.
   ⚠ AND IT IS A REAL BUTTON, NEVER A `title=` TOOLTIP: a native tooltip is invisible on touch
   (owner ruling 2026-07-18). The word `edited` rather than reddit's `*` — a one-character button
   is a sub-minimum touch target, and the reveal contains TEXT, so the control is a door and a door
   should be labelled. */
.cedited{position:relative;background:none;border:none;padding:2px 4px;margin-left:2px;cursor:pointer;
  font:inherit;font-size:10.5px;color:var(--faint);
  text-decoration:underline dotted;text-underline-offset:2px}
.cedited:hover,.cedited[aria-expanded="true"]{color:var(--accent)}
.cedited:focus-visible{outline:1px solid var(--accent);outline-offset:1px}
/* ⚖ 60x30 DRAWN, 60x44 TAPPED (forage-fxj.1, 2026-08-21). The padding below already grew it once
   and stopped 14px short; the rest is bought as a HIT AREA, because `.cedited` sits INLINE INSIDE
   `.cmeta` (MEASURED 298x30 at 390x844) and 14 more drawn pixels would push every comment's header
   apart — the floor may not change what a member can see. ⚠ VERTICAL ONLY: the metaline reads
   `① qaowner (you) edited ▾` left to right, so a horizontal halo would reach the author link one
   side and `.cfold` the other. 7px each way lands on the comment's own top edge and on `.ctext`,
   which is prose. */
@media(pointer:coarse){
  .cedited{padding:7px 9px;margin-left:0}
  .cedited::after{content:"";position:absolute;left:0;right:0;top:50%;transform:translateY(-50%);
    height:var(--tap-min)}
}
/* the reveal — deliberately the SAME body-appended, page-positioned idiom as .cquote-preview four
   lines up, so this file's two hover surfaces behave identically instead of inventing a second
   popover model. ⚠ pointer-events stay ON, unlike the quote preview: on touch the panel has to
   survive the tap that opened it. Its >>refs are inert (plainRefs), so there is nothing to click. */
.cedited-panel{position:absolute;z-index:var(--z-tooltip);max-width:340px;background:var(--panel);
  border:1px solid var(--hair-hi);padding:8px 10px;box-shadow:0 8px 24px -8px rgba(0,0,0,.7);
  font-size:12px;color:var(--ink)}
.cedited-panel .cedited-lbl{font-size:10px;text-transform:var(--caps);
  letter-spacing:calc(.08em * var(--track));color:var(--faint);margin-bottom:4px}
.cedited-panel .cedited-none{color:var(--faint);font-style:italic}
/* ⚠ AND IT HAS TO BE RAISED WHILE THE VIEWER IS UP, OR THE PHONE — THE SURFACE R12 WAS WRITTEN
   FOR — NEVER SEES IT. The panel is appended to <body>, and `.vw-root` is a body-level
   position:fixed box at --z-viewer (1200); at --z-tooltip (500) the reveal renders BEHIND the
   sheet whose button opened it, so a tap did nothing and there was nothing to notice. Same
   re-raise idiom, same modifier, as .modal-backdrop and the points juice under body.vw-lock
   (app-viewer.css) — one rule, scoped to the one state that needs it. ⚠ .cquote-preview above
   has the identical exposure and is left alone deliberately: it is hover-only, it predates this
   control, and fixing it is not this change's to make. */
body.vw-lock .cedited-panel{z-index:var(--z-sheet)}
/* the inline editor: the body swaps for this IN PLACE and nothing navigates */
.cedit{margin:4px 0}
.cedit-ta{width:100%;box-sizing:border-box;background:var(--sunk);color:var(--ink);
  border:1px solid var(--hair-hi);padding:6px 8px;font:inherit;font-size:12.5px;line-height:1.55;
  resize:vertical}
.cedit-acts{display:flex;gap:12px;margin-top:5px}
/* the "edit" affordance rides `.clink` for its colour and weight and adds a target on top of it —
   its bar on the phone sheet (.vw-cacts) is a row of 44px reaction buttons, and a bare 10.5px word
   dropped in beside them is the smallest thing on the screen. */
.cedit-open{padding:5px 3px}
@media(pointer:coarse){.cedit-open{padding:8px 6px}}
.cedit-msg{margin-top:4px;font-size:10.5px;color:var(--bad)}
.cedit-msg:empty{display:none}
/* quote-target flash highlight (~1.2s) */
@keyframes cflash{0%{background:rgba(237,167,51,.34)}100%{background:transparent}}
.comment.flash{animation:cflash 1.3s ease-out}
@media(prefers-reduced-motion:reduce){.comment.flash{animation:none;background:rgba(237,167,51,.18)}}
/* thread rails + collapse */
.cthread{display:flex;gap:0;margin-top:12px}
.cthread:first-child{margin-top:0}
.cthread>.comment,.cthread>.cfolded{flex:1;min-width:0}
.crail{flex:none;width:4px;align-self:stretch;border:none;padding:0;margin-right:9px;cursor:pointer;
  background:var(--rail,var(--hair-hi));opacity:.5;border-radius:2px}
.crail:hover{opacity:1}
/* ⚖ THE 44px FLOOR ON A HAIRLINE (2026-08-09; the rule is `--tap-min` in app.css). A 4px-wide
   button is not a target, and this one collapses a comment thread. But 44px of it would be a
   COLUMN down every thread, and a transparent 44px hit box would reach the next rail one indent
   step away (4px rail + 9px gap = 13px) and collapse the wrong one. So it eats its own margin:
   13px of target — the whole of the space it owns — with the 4px bar painted by a gradient so
   nothing on screen moves. Still under the floor, and recorded as a waiver rather than hidden.
   Here and not in app.css because `width`/`background` lose a same-specificity fight to the
   base rule three lines up; only min-* travels across parts. */
@media(pointer:coarse){
  .crail{width:13px;margin-right:0;
    background:linear-gradient(to right,var(--rail,var(--hair-hi)) 0 4px,transparent 4px)}
}
.cfold{position:relative;margin-left:auto;background:none;border:none;color:var(--faint);cursor:pointer;
  font-size:11px;padding:0 2px;line-height:1;flex:none}
.cfold:hover{color:var(--accent)}
.cfolded{position:relative;display:block;width:100%;text-align:left;background:none;border:none;
  color:var(--dim);font:inherit;font-size:12px;cursor:pointer;padding:7px 4px}
.cfolded:hover{color:var(--ink)}
/* ⚖ THE THREAD'S TWO OTHER TARGETS, BOUGHT AS HIT AREAS (forage-fxj.1, 2026-08-21). Both numbers
   are MEASURED on /post at 390x844 with touch, viewer closed, on a real thread four deep.
   · `.cfold` is **11x11** — the joint worst control in the whole census, and it collapses a
     thread. It takes the CORNER HALO (`.pin-unpin::after`, app.css) rather than the vertical
     overhang, because it is the last thing on the metaline with nothing to its right: measured,
     the nearest control on that line is `edited` **127px** away, so 16.5px of halo each way
     reaches nobody. Growing the ▾ itself would put an 11px glyph at 44px in the middle of a
     10.5px metaline.
   · `.cfolded` is **325x33** and `width:100%`, so it is only short VERTICALLY — 5.5px each way.
     ⚠ IT MEASURES **0px** FROM `.crail`, flush, which is the one pair in this thread that a
     horizontal halo would ruin: the rail is 13px of target that collapses the thread and the stub
     is what re-opens it. Vertical only, here for that reason and not by habit. */
@media(pointer:coarse){
  .cfold::after{content:"";position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);
    width:var(--tap-min);height:var(--tap-min)}
  .cfolded::after{content:"";position:absolute;left:0;right:0;top:50%;transform:translateY(-50%);
    height:var(--tap-min)}
}
/* composer explicit-suggest chips (opt-in, default OFF) */
.csuggest{display:flex;flex-wrap:wrap;gap:6px;align-items:center;margin-top:7px}
.csuggest-lbl{font-size:9.5px;color:var(--faint);text-transform:var(--caps);letter-spacing:calc(.1em * var(--track))}
.csuggest-chip{border:1px dashed var(--hair-hi);background:none;color:var(--dim);
  font-size:11px;padding:2px 8px;cursor:pointer}
.csuggest-chip.on{border-style:solid;border-color:var(--accent);color:var(--accent);background:rgba(237,167,51,.08)}
/* double-tap heart burst. It is JUICE, not a dialog: appended to <body> at the tap point and
   pointer-events:none, exactly like .pointfloat. It used to carry an 80 — a dead tie with
   .modal-backdrop, resolved by nothing but DOM order — which said a decorative heart may cover
   a confirm dialog. It cannot happen (a backdrop swallows the tap before the media sees it),
   but the number claimed it could. Ranked with the rest of the celebration layer instead. */
.dt-heart{position:absolute;z-index:var(--z-juice);transform:translate(-50%,-50%);font-size:46px;color:#ff5a7a;
  pointer-events:none;text-shadow:0 0 14px rgba(255,90,122,.6);animation:dt-heart .8s ease-out forwards}
@keyframes dt-heart{
  0%{opacity:0;transform:translate(-50%,-50%) scale(.4)}
  25%{opacity:1;transform:translate(-50%,-50%) scale(1.15)}
  100%{opacity:0;transform:translate(-50%,-50%) scale(1.5) translate(0,-32px)}}


/* ════ THE SITE-WIDE VOLUME BAR (owner ask 2026-08-13, volumeBar() in common-profiletabs.js) ══
   Sits on the masthead beside the score chip, governs every <video> on the page through the
   shared `qvol` preference. ⚠ DESKTOP ONLY — it is built behind a (hover:hover) capability
   test, not a width query, so a narrow desktop window keeps it and a wide tablet does not.
   The hover card deliberately has no control of its own (one was removed 2026-07-21o because
   the card animates out from under the cursor); this is what governs the preview instead. */
.volbar { display: flex; align-items: center; gap: 6px; margin-right: 10px; }
.volbar .volmute {
  display: grid; place-items: center; width: 30px; height: 30px; padding: 0;
  background: none; border: 0; color: var(--dim); cursor: pointer;
}
.volbar .volmute:hover { color: var(--ink); }
.volbar.muted .volmute { color: var(--bad); }
.volbar .volmute .navi { width: 18px; height: 18px; }
/* the track is deliberately short — it is a trim, not a mixing desk, and the masthead is the
   one row on the site where every extra pixel pushes a nav entry into the burger */
.volbar .volrange { width: 84px; accent-color: var(--accent, var(--gold, #d9a441)); cursor: pointer; }
.volbar.muted .volrange { opacity: .45; }
@media (max-width: 999px) { .volbar { display: none; } }

/* ════ THE BOARD — subShreddits, `/s` (.scratch/feedback-forum/DESIGN.md REVISION 1 + 2) ════
   ⚖ D5 — A SUBSHREDDIT'S LISTING IS `/hot`'s BOARD-INDEX ROW (owner ruling 2026-07-19, the
   4chan board-index review quoted at web/page-hot.js:6-12), text threads included, drawn with
   an EMPTY THUMB SLOT exactly as old reddit drew a self post. It lives in THIS sheet, beside
   `/hot`, because it is the same row family and a change to one is a question about the other.

   ⛔ AND IT DELIBERATELY DOES NOT REUSE `/hot`'s MOBILE RULE — the `@media(max-width:640px)`
   block ~150 lines above, which stacks `.hotthumb` into a full-width banner. That is right for
   `/hot`, whose job is one loud post per screen. In a MIXED listing it is wrong by a factor of
   three: measured on the real stylesheets at 390x844, a stacked media row is 390px and a text
   row is 118px, so the two kinds of thread stop looking like one list and start looking like
   two lists interleaved. The slot is a FIXED SQUARE THAT NEVER STACKS — a fixed column
   whatever fills it, so the row's height never depends on which kind of thread it is.

   ⚖ ONE RENDERER, TWO WIDTHS — NOT TWO VIEWS. Desktop is the SAME rows at full-bleed width.
   A switchable listing view is dead three times over: closed roads #100, #94, #106.

   ⚖ WHERE THE BOARD'S LOOK ACTUALLY LIVES NOW: `web/s-forum.css`, THE ONE NEW PART, LINKED BY
   `web/s.html` ALONE (2026-08-16, the forum visual-language pass). This sheet keeps only the
   rules the composer, the picture slot and the flags need — the ROW, the SLOT, the board bar,
   the tab strip, the thread head and the reply column were MOVED there, not copied, so there
   is exactly one description of each. Do not re-add a `.sbrow`/`.sbslot`/`.sbtitle`/`.sbmeta`
   rule here: the two sheets would then disagree and s-forum.css, loading last, would win
   silently. ⚠ `app.css` was AT its 1,000-line ceiling (repealed 2026-08-29) and could not hold it; a new `app-*.css`
   would have had to join all 26 heads (cssparts.test.js), which the scope forbade. */
.sbwrap{max-width:none;margin:0;padding:0 18px}
/* ⚠ the ≥1800px `padding-right:304px` rail-dodge is DELETED (forage-gof.33): ≥900px the board
   is a centered 720px column now — s-forum.css §7, which loads later, owns the desktop frame.
   (`.hotwrap`'s twin above is /hot's and stays: that surface is genuinely full-bleed.) */

/* ---- `/s`, the subShreddits page (D7): followed on top, everything else below ---- */
.sbgroup{font-size:10px;letter-spacing:calc(.18em * var(--track));text-transform:var(--caps);
  color:var(--faint);padding:18px 2px 8px}
/* ⚠ `.sbdirlist` / `.sbdirrow` / `.sbdirname` and the colour dot are in web/s-forum.css. */
.sbdirhead{display:flex;flex-wrap:wrap;align-items:baseline;gap:8px}
.sbdirtitle{color:var(--bright);font-size:13px}
.sbdirblurb{color:var(--ink);font-size:12.5px;line-height:1.55;margin-top:6px;
  overflow-wrap:anywhere}
.sbdirmeta{color:var(--dim);font-size:11px;margin-top:6px;font-variant-numeric:tabular-nums}
.sbjoined{color:var(--good)}
/* the "N new" count (D7). ⚠ A COUNT IS NOT A NOTIFICATION — it is deliberately a quiet chip
   in the row and never a red dot on the masthead; following is what opts you into a
   subShreddit's notifications, and this is the second, quieter channel. */
.sbnew{color:var(--accent);border:1px solid var(--hair-hi);font-size:10px;padding:1px 6px;
  letter-spacing:calc(.06em * var(--track));font-variant-numeric:tabular-nums}
/* a fact about the subShreddit, not a control: triaged / archived / announcements only.
   ⚠ --dim RATHER THAN --faint, AND IT WAS MEASURED, NOT PREFERRED. On the two paper skins
   --faint lands at ~3:1 against the cream (yotsuba #9b8a82 on #ffffee, yotsuba-b #7a84a3 on
   #eef2ff) and the chip read as an empty dashed box in the render. These words are the only
   place a member is told a subShreddit is triaged, locked or archived — the same rule as the
   triage word in the metaline: the WORD is the signal, so it has to be readable. */
.sbflag{color:var(--dim);border:1px dashed var(--hair-hi);font-size:9.5px;padding:1px 6px;
  letter-spacing:calc(.1em * var(--track));text-transform:var(--caps)}

/* ---- the subShreddit's own prose. ⚠ THE HEADER AROUND IT IS `.sbbar` IN web/s-forum.css:
   the name, the follow control and D6's sort became a 48px board bar, and the blurb + rules
   moved behind that bar's disclosure (they cost 125px on every visit before a single thread). */
.sbblurb{color:var(--ink);font-size:12.5px;line-height:1.6;margin-top:8px;max-width:70ch}
/* the RULES are a disclosure, not a wall: a charter nobody folded away is a charter that
   pushes the first thread off a phone screen. */
.sbrules{margin-top:8px;max-width:70ch}
.sbrules summary{color:var(--dim);font-size:11px;letter-spacing:calc(.08em * var(--track));
  text-transform:var(--caps);cursor:pointer;min-height:24px}
.sbrules .ctext{color:var(--dim);font-size:12px;line-height:1.6;margin-top:6px}

/* ⚠ THE ROW, THE SLOT, THE TITLE AND THE METALINE ARE IN web/s-forum.css — moved 2026-08-16,
   not copied. Only the colour words the metaline uses stay here, beside the ones `/hot` uses. */
/* a metaline SEGMENT — the separator and the thing it introduces, kept on one line. See
   sbBit() in web/page-s.js: without it the phone breaks after the `·` and orphans the word. */
.sbbit{white-space:nowrap}
.sbdot{color:var(--faint)}
/* ⚖ GREEN IS EARNED, NEVER THE DEFAULT (owner, 2026-08-16: *"the zero is green"*). This was
   `--good` unconditionally with a `neg` override, so a standing of ZERO — the archive saying
   nobody has voted — was painted in the same colour as a thread everybody upvoted. The base is
   the metaline's own quiet token; `pos` is what turns it green and `neg` red. A zero is not
   drawn at all now (page-s.js), so this rule is the SECOND line of defence rather than the
   first: it is what stops a zero that slips back through from reading as a positive standing. */
.sbstanding{color:var(--dim)}
.sbstanding.pos{color:var(--good)}
.sbstanding.neg{color:var(--bad)}
/* THE WORD BESIDE THE COLOUR, NEVER COLOUR ALONE, and never a native title= (invisible on
   touch, owner ruling 2026-07-18). Drawn only where the subShreddit is triaged, and only where
   the status is not the default `open` — see sbRow(). */
.sbstate{text-transform:var(--caps);letter-spacing:calc(.08em * var(--track));font-size:10px}
.sbstate.st-open{color:var(--info)}
.sbstate.st-confirmed{color:var(--accent)}
.sbstate.st-fixed{color:var(--good)}
.sbstate.st-declined,.sbstate.st-duplicate{color:var(--faint)}
.sbmore{display:flex;justify-content:center;padding:18px 0}

/* ---- THE COMPOSER — raised in place, never a page you navigate to ---- */
.sbcompose{padding:12px 2px}
.sbcompose-panel{display:flex;flex-direction:column;gap:10px;margin-top:12px;
  border:1px solid var(--hair);background:var(--panel);padding:14px}
.sbcompose-title,.sbcompose-body{width:100%;background:var(--sunk);border:1px solid var(--hair);
  color:var(--ink);font:inherit;font-size:13px;padding:10px;min-height:44px}
.sbcompose-body{line-height:1.55;resize:vertical}
.sbcompose-body:disabled{color:var(--faint);background:var(--void)}
.sbcompose-attach{display:flex;flex-wrap:wrap;align-items:center;gap:10px}
.sbcompose-chip{display:flex;align-items:center;gap:8px;border:1px solid var(--hair-hi);
  padding:6px 8px;min-width:0}
.sbcompose-thumb{width:44px;height:44px;object-fit:cover;object-position:top;display:block}
.sbcompose-label{font-size:11px;color:var(--dim);overflow-wrap:anywhere}
.sbcompose-drop{background:none;border:0;color:var(--bad);font:inherit;cursor:pointer;
  min-width:44px;min-height:44px}
.sbcompose-priv{display:flex;align-items:center;gap:8px;font-size:11px;color:var(--dim);
  min-height:32px;cursor:pointer}
.sbcompose-send{align-self:flex-start}
.sbcompose-msg{color:var(--bad)}
/* ⚠ THE ONE HONEST SENTENCE ABOUT WHAT IS NOT BUILT. It is prose, never a disabled control:
   a button that exists and does nothing is the dead ⊕ this archive already shipped once
   (owner report 2026-07-29, three surfaces). */
.sbunbuilt{border-left:2px solid var(--hair-hi);padding-left:10px}

/* ---- D4: A PICTURE FROM YOUR PHONE, ON A THREAD OR A REPLY ----
   ⚖ ONE BLOCK, TWO COMPOSERS — web/page-s-thread.js's sbPicSlot() is the single control and
   these are the only rules it needs, so the thread composer and the reply box cannot look like
   two features. Deliberately NOT reusing .sbcompose-chip: that chip labels an ARCHIVE POST (a
   thumb beside its title) and this one is a bare picture the member just added, with no title to
   show and no /post link to promise — same shape, different claim.
   ⚠ EVERY TAP TARGET IS 44px (phone-first, and the ✕ sits next to a thumb). The note under it is
   prose because it makes a CLAIM the member cannot otherwise check — that this picture is not
   going into the archive — and the message line is --dim, never --bad: the commonest thing it
   ever says is the dedupe line, which is good news. */
.sbpic{display:flex;flex-wrap:wrap;align-items:center;gap:10px;position:relative}
.sbpic-add{min-height:44px}
.sbpic-chip{display:flex;align-items:center;gap:8px;border:1px solid var(--hair-hi);
  padding:6px 8px;min-width:0}
.sbpic-thumb{width:44px;height:44px;object-fit:cover;object-position:top;display:block}
.sbpic-drop{background:none;border:0;color:var(--bad);font:inherit;cursor:pointer;
  min-width:44px;min-height:44px}
.sbpic-msg{flex-basis:100%;color:var(--dim)}
.sbpic-note{border-left:2px solid var(--hair-hi);padding-left:10px}

/* ---- ONE THREAD ---- */
.sbthread{padding:14px 2px;max-width:900px}
.sbcrumb{font-size:11px;color:var(--dim);margin-bottom:10px}
.sbcrumb-a{color:var(--accent);text-decoration:none}
.sbcrumb-a:hover{text-decoration:underline}
.sbthreadmeta{margin-bottom:12px}
.sbedited{color:var(--faint)}
/* ⚠ `.sbthreadtitle`, `.sbmedia` (now full-bleed, with its kind·dimensions badge) and
   `.sbbody` are in web/s-forum.css. */
.sbgone{color:var(--faint);border:1px dashed var(--hair-hi);padding:14px;margin:6px 0 14px;
  font-size:12px}
.sbreplies{margin-top:8px;border-top:1px solid var(--hair);padding-top:14px}
.sbrepliesh{font-size:10px;letter-spacing:calc(.18em * var(--track));text-transform:var(--caps);
  color:var(--faint);margin-bottom:10px}
.sbreplyact{display:flex;flex-wrap:wrap;align-items:center;gap:10px;margin-bottom:14px}
/* ⚖ THE `.sbreply-*` CELL IS GONE, AND ITS REMOVAL IS THE POINT (R15 slice, 2026-08-16). It
   was a read-only look for a read-only slice; replies are now votable, answerable, editable and
   deletable, and a reply on the board IS a comment on a post. So the thread view renders the
   ARCHIVE'S OWN comment shape — `.comment` / `.cvote` / `.cmain` / `.cmeta` / `.cactions` /
   `.creplies` / `.cform`, every one of them already in app-profile.css, which s.html loads. A
   second set of classes for the same object is how the vote widget ended up with two
   implementations that disagreed about one payload. This list only has to hold them. */
/* ⚠ `.sbreplylist` (now a stack of `.cthread` chains with coloured branch rails) and
   `.sbthreadacts` (now a counts-left / verbs-right split) are in web/s-forum.css. */
/* the body's holder exists so the inline editor can swap the rendered `.ctext` IN PLACE
   (comments.js editControl looks for one inside the element it was handed). */
.sbbodywrap{margin-bottom:6px}
