/* app-profile.css — the rich profile, PROFILE v2 and SOCIAL v1.

   ⚖ 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 2071-2428). 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. */
/* =====================================================================
   THE RICH PROFILE (profiles v1) — /u/<username>. Reuses dossier atoms
   (.rail-score, .rail-track, the .dsr- family, .tile) in a full-page layout.
   Terminal theme; light-on-dark; reduced-motion has nothing to disable
   here (no animation of its own beyond the reused .rail-fill transition).

   ⚠ THAT LINE NAMED THE .dsr FAMILY WITH A STAR AND A SLASH, AND THOSE TWO CHARACTERS ENDED
   THIS COMMENT (found 2026-08-20). They are a close-comment marker wherever they appear, prose
   or not, so everything from there to the marker below was parsed as CSS — and a CSS parser
   recovering from garbage swallows tokens up to the next brace block, which was
   `.ulink{color:var(--accent)}` on the line under it. That is the only definition of the base
   username colour in the client (app-rank.css cites it by name as the rule its own overrides
   sit on top of).
   ⚖ AND IT WAS INVISIBLE FOR A SECOND REASON, WHICH IS THE REALLY INSTRUCTIVE HALF: on the
   default terminal skin `--link` IS `--accent`, so `a{color:var(--link)}` produced the same
   pixel and nothing looked wrong. Under the six SKINS it does not — measured in Chromium
   2026-08-20, a username on `yotsuba` computed rgb(0,0,238), the generic link blue, where the
   skin's accent is #af0a0f. So the bug only ever showed on a skin, which is the least-used
   path, in a colour nobody would call obviously wrong. cssparts.test.js scans for the
   unbalanced marker now.
   ⚠ Never write those two characters next to each other inside a comment.

   ⚖ AND PUTTING IT BACK BROKE THE RANK LADDER, WHICH IS THE OTHER HALF OF THE SAME BUG
   (forage-fk5, viewer_e2e.sh 324/326). This rule was dead from 2026-07-17 — the day usernames
   shipped — to 2026-08-20, so `.rk-<tier>` painted every username by default and NOTHING was
   ever written to make it win a fight it never had. Alive, `.ulink` (0,1,0) beats the one-class
   ladder rule (0,1,0) on source order, because app-rank.css loads before this file. Every name
   went flat accent while the avatar ring beside it — `.vw-cav`, which takes `currentColor` off
   that same `rk-` class — kept the tier, and app-viewer.css says in as many words that those two
   "can never disagree".
   ⚖ THE RULING (owner, 2026-08-20): A RANK COLOUR WINS ON A USERNAME. `.ulink` is the FALLBACK —
   for a link to a member with no tier (the caller passed no `score`: admin tables, invite rows,
   credited names) and for every other place the class is used. The ring, the name and the pip
   are one statement; flat accent erases the information. The fix is app-rank.css's business and
   it is the idiom this app already uses everywhere else an identity beats the base link
   treatment: the ladder is written `.rk-<tier>,.ulink.rk-<tier>` — one rule, one copy of each
   hex, the two-class half at (0,2,0). Exactly `.ulink.apex-name`, `.ulink.binkie-name`,
   `.ulink.scion-name`, `.prof-badge.apex-pill` and the `.ulink.rk-*:hover` row, all of which
   exist for this reason and are pinned by scioncolour.test.js.
   ⛔ REJECTED, and why, so nobody retries them:
   · `!important` — there is a plain specificity answer, so it would only hide the tie.
   · re-ordering the parts so app-rank.css loads last — the order IS the cascade (cssparts.test.js
     pins it on all 18 pages) and app-viewer.css must keep the last word. A behaviour change
     wearing a fix's clothes.
   · narrowing this rule instead, `.ulink:not(.rk-lurker,…)` — it lands at (0,2,0), which TIES
     `.ulink.apex-name` and wins on source order, so the apex/binkie/scion names all go accent.
     Every identity class would have to be listed here, in the wrong file, forever.
   · `a:where(.ulink)` (0,0,1) — one line and it does work, but it demotes the base username
     colour below EVERY class that can land on the same element, not just the ladder. That is a
     site-wide cascade change to fix one ladder.
   ===================================================================== */
.ulink{color:var(--accent)}
.ulink:hover{color:var(--bright);text-decoration:underline}
.profile-view{max-width:940px;margin:0 auto;padding:6px 0 40px}

/* identity header */
.prof-head{display:flex;gap:20px;align-items:flex-start;padding:4px 0 18px;border-bottom:1px solid var(--hair)}
.prof-avatar{flex:0 0 auto;width:128px;height:128px;border:1px solid var(--hair-hi);background:var(--sunk);
  display:flex;align-items:center;justify-content:center;position:relative;overflow:hidden}
.prof-avatar.empty{}
.prof-avfallback{position:absolute;width:56%;height:56%;object-fit:contain;filter:invert(1);opacity:.14}
.prof-avlink{position:absolute;inset:0;display:block}
.prof-avimg{width:100%;height:100%;object-fit:cover;display:block}
.prof-idcol{flex:1;min-width:0}
.prof-namerow{display:flex;align-items:baseline;flex-wrap:wrap;gap:10px}
.prof-name{font-size:26px;line-height:1.1;color:var(--bright);letter-spacing:calc(.02em * var(--track));margin:0}
.prof-role{font-size:9px;letter-spacing:calc(.16em * var(--track));text-transform:var(--caps);padding:2px 8px;
  border:1px solid var(--hair-hi);color:var(--dim)}
.prof-role.role-owner{color:var(--accent);border-color:var(--accent-deep)}
.prof-role.role-admin{color:var(--info);border-color:var(--info)}
.prof-badge{font-size:8.5px;letter-spacing:calc(.14em * var(--track));text-transform:var(--caps);padding:2px 7px;border:1px solid var(--hair-hi);color:var(--dim)}
.prof-badge.trusted{color:var(--good);border-color:var(--good)}
.prof-badge.you{color:var(--accent);border-color:var(--accent-deep)}
.prof-rankrow{margin-top:7px;display:flex;gap:12px;align-items:baseline;flex-wrap:wrap}
.prof-rank{font-size:11px;letter-spacing:calc(.16em * var(--track));text-transform:var(--caps);color:var(--accent)}
.prof-since{font-size:10.5px;color:var(--faint);letter-spacing:calc(.04em * var(--track))}
.prof-motto{margin-top:9px;font-size:14px;color:var(--ink);font-style:italic;line-height:1.45;
  overflow-wrap:anywhere}
.prof-motto.ghost{color:var(--faint);font-style:normal}
.prof-motto-wrap{margin-top:9px;display:flex;align-items:baseline;gap:10px;flex-wrap:wrap}
.prof-motto-edit{font-size:9px;letter-spacing:calc(.1em * var(--track));text-transform:var(--caps);color:var(--accent);
  border:1px solid var(--hair);padding:2px 7px}
.prof-motto-edit:hover{border-color:var(--accent)}
.prof-mint{margin-top:9px;display:flex;align-items:baseline;gap:10px;flex-wrap:wrap}
.prof-mint .prof-mint-msg{margin:0}

/* ═══ THE STEAM CARD (owner ruling 2026-09-02) ══════════════════════════════════════════════
   Verbatim: *"the link steam section on the profile is pretty basic looking. you can do better. a
   lot of the UI has the same design language and text bleeds together. the only thing separating
   STEAM is the capitals."*

   ⚑ REPLACED. What stood here was *"THE SAME SHAPE AS .prof-mint one line up, deliberately"* — a
   micro-label, a state string, two ghost buttons and a footnote, drawn exactly like the eleven
   other rows on this page. That argument ("a second layout for the same idea is a second thing to
   keep true") is sound for two of the archive's OWN controls, and it is wrong here: this row is
   not the archive talking about itself, it is the archive standing next to somebody else's
   service. It has to look like a different kind of thing, because it IS one.

   ⚖ THE SIGNATURE IS A PLAQUE, AND IT IS THE ONE LOUD THING IN THE SECTION. Valve's mark at 26px
   on a 44px tile, in Steam's own blue on Steam's own navy — the only place in this archive that
   spends a colour it did not choose. Everything else on the card is quiet: the word in the UI face
   at 16px instead of a 9.5px tracked capital, one state line, and text actions with no boxes.
   ⛔ SPEND THE BLUE ONCE. `.sc-artmarki` (clubs.css) draws the same mark at 16px in `--dim` with
   the archive's other furniture and must not be repainted: a brand colour used twice is a brand
   colour, used everywhere is a theme.

   ⛔ AND THE PLAQUE'S TWO COLOURS ARE HARDCODED, NOT SKIN VARS, WHICH IS DELIBERATE AND MEASURED.
   `#66c0f4` on `--raised` is 8.66:1 on the terminal skin and **1.24:1 on yotsuba** (`#f0c0b0`) —
   invisible. A brand mark cannot be re-hued per skin without ceasing to be the brand mark, so it
   carries its own known ground the way the casino's gold does (app.css, the MONEY COLOUR note):
   `#66c0f4` on `#1b2838` measures **7.4:1** in every skin, forever, because neither value moves.
   ⚑ THE `off` TINT WAS RAISED 2026-09-02, `#4883a9` → `#5695bf`. The old value was 3.62:1 — over
   the 3:1 a meaningful graphic is owed, under the 4.5 the redraw put on everything the plaque
   carries. `#5695bf` on the same ground is **4.58:1**, and "dimmed" survives it (4.58 vs 7.40).

   ═══ ONE GRID (owner ruling 2026-09-02, verbatim: *"the steam card layout is pretty trash. why is
   nothing aligned?"*) ═══════════════════════════════════════════════════════════════════════════
   ⛔ WHAT WAS WRONG, MEASURED AT 390px. The card was `grid-template-columns:46px minmax(0,1fr)`:
   the word and the field sat in column 2 at x≈176, while the button, the reason and the two
   actions were `grid-column:1 / -1` and began at the plaque's own edge, x≈58 — and the card's
   padding edge was a third. THREE left edges on one small card, which is the whole complaint.
   ⚖ THE FIX IS THAT THE PLAQUE NO LONGER OWNS A COLUMN. The card is one flex column: a HEADER row
   that contains the tile, and a BODY that contains everything else. Nothing below the header is
   keyed off the tile's width, so every body row starts at the card's single padding edge.
   `.prof-steam-body` is that promise made structural — a row that wants to line up has exactly one
   place it can be put, and there is no longer a `grid-column` for it to be put in wrongly.
   ⛔ AND THE VERTICAL DIVIDER IS GONE. `.prof-steam-who` used to hang off a `border-left` that
   lined up with nothing above it and nothing below it. Who is linked now sits at the header's
   RIGHT end — an edge the card actually has.
   ⚠ PINNED BEHAVIOURALLY: clubs_writes_e2e §8's ALIGNMENT leg reads `getBoundingClientRect().left`
   off every visible child of `.prof-steam-body` AND off the header tile, in all three states, and
   requires one value ±1px. A regex over this file could not have caught the bug it replaces. */
.prof-steam-card{
  margin-top:10px;
  display:flex; flex-direction:column; gap:12px;
  padding:16px; box-sizing:border-box;
  background:var(--raised); border:1px solid var(--hair-hi); border-radius:3px;
}
/* ROW 1 — the tile, the word, and (linked only) who. `align-items:center` sets the 16px word on
   the tile's centre line; the tile is the header's FIRST child and carries no margin, so its left
   IS the card's padding edge — the same edge every body row starts at. */
.prof-steam-head{display:flex;align-items:center;gap:12px;min-width:0}
.prof-steam-plaque{
  flex:0 0 auto;
  width:44px; height:44px; box-sizing:border-box;
  display:flex; align-items:center; justify-content:center;
  background:#1b2838; border-radius:3px; color:#66c0f4;
}
.prof-steam-card[data-steam="off"] .prof-steam-plaque{color:#5695bf}
.prof-steam-plaque svg{width:26px;height:26px;display:block}
/* ⚖ THE WORD, IN THE UI FACE AND IN SENTENCE CASE — the owner's *"the only thing separating STEAM
   is the capitals"*. It is 16px against the page's 9.5px tracked micro-labels, and `text-transform`
   and `letter-spacing` are reset rather than left to inherit `--caps`/`--track`: this is a proper
   noun somebody else owns, not one of the archive's own labels. */
.prof-steam-name{
  font-family:var(--ui);font-size:16px;font-weight:600;line-height:1.1;
  color:var(--bright);text-transform:none;letter-spacing:0;
}
/* WHO IS LINKED — at the header's RIGHT END. ⛔ No `border-left`: see the ONE GRID note above.
   ⚖ AND THE PICTURE IS SQUARE (owner ruling 2026-09-02, verbatim: *"steam pfps are square not
   circles"*). Valve serves a square avatar and the archive draws pictures beside names square; a
   50% radius here was this card inventing a shape neither side uses. 2px is the hairline corner
   the rest of the page wears, and the gate holds it at ≤2px. */
.prof-steam-who{display:flex;align-items:center;gap:8px;min-width:0;margin-left:auto}
.prof-steam-av{width:28px;height:28px;border-radius:2px;display:block;object-fit:cover;
  background:var(--sunk);border:1px solid var(--hair-hi)}
.prof-steam-persona{font-size:12.5px;color:var(--ink);text-transform:none;letter-spacing:0;
  white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:14ch}
/* ROWS 2+ — THE BODY, AND THE ONE EDGE. ⚖ Every direct child is stretched to the card's full
   inner width, so each one's left is the card's padding edge whatever it contains. ⛔ NOTHING HERE
   MAY BE `justify-self`d, given a `grid-column`, or sized off the plaque — that is the defect this
   replaced. 10px between rows, 12px from the header (`.prof-steam-card`'s own gap). */
.prof-steam-body{display:flex;flex-direction:column;align-items:stretch;gap:10px;min-width:0}
.prof-steam-state{font-size:12px;line-height:1.45;color:var(--dim);
  text-transform:none;letter-spacing:0;margin:0}
/* ⚖ TEXT ACTIONS, NOT BUTTONS (the ruling: *"two quiet text actions"*). The hairline over them is
   full-BODY width, so it is one more thing on the card's single edge rather than a rule that ends
   somewhere arbitrary. */
.prof-steam-acts{display:flex;align-items:center;gap:9px;
  padding-top:10px;border-top:1px solid var(--hair)}
.prof-steam-act{font-size:11.5px;color:var(--dim);text-transform:none;letter-spacing:0;
  background:none;border:0;padding:0;cursor:pointer;
  text-decoration:underline;text-underline-offset:3px;text-decoration-color:var(--hair-hi)}
.prof-steam-act:hover{color:var(--bright);text-decoration-color:var(--bright)}
.prof-steam-act:focus-visible{outline:1px solid var(--accent);outline-offset:3px}
.prof-steam-act:disabled{color:var(--faint);cursor:default;text-decoration-color:transparent}
.prof-steam-sep{font-size:11px;color:var(--faint)}
/* the paste row: ONE field on its own row, full width. ⚠ The button is NOT in here any more — it
   was, and a wrapping flex row is exactly how the two came to sit on different left edges at
   different widths. Two body rows, one edge, at every width. */
.prof-steam-form{display:flex;gap:8px;align-items:center}
.prof-steam-input{flex:1;min-width:0;font-size:13px}
/* ⚖ FULL WIDTH ON A PHONE, its own size on a desktop — and its LEFT is the body edge either way,
   which is the only property the alignment gate cares about. */
.prof-steam-go{
  display:inline-flex;align-items:center;justify-content:center;gap:9px;
  width:100%; padding:10px 15px; cursor:pointer;
  background:var(--accent);border:1px solid var(--accent);color:var(--void);
  font-size:11.5px;letter-spacing:calc(.09em * var(--track));text-transform:var(--caps)}
.prof-steam-go:hover{background:var(--accent-deep);border-color:var(--accent-deep)}
.prof-steam-go:disabled{opacity:.55;cursor:default}
.prof-steam-go svg{width:15px;height:15px;display:block}
@media(min-width:1000px){
  .prof-steam-go{width:auto;align-self:flex-start}
}
/* ⚠ THE REASON, NOT A WARNING, and it lives on the card only while there is something to press:
   a member who already linked never needs to read it again. */
.prof-steam-why{margin:0;font-size:12px;line-height:1.5;color:var(--faint);
  text-transform:none;letter-spacing:0;max-width:52ch}
.prof-steam-msg{margin:0}
.prof-steam-msg:empty{display:none}
/* ═══ THE 44px FLOOR, APPLIED (app.css --tap-min · server/tests/mobiletaps.test.js) ═════════
   ⚖ TWO CONTROLS, TWO SHAPES, AND THE DIFFERENCE IS WHETHER THE DRAWN BOX MAY GROW.
   · `.prof-steam-go` is a button with nothing under it, so it simply grows to the floor and
     nothing moves around it.
   · `.prof-steam-act` may NOT grow — docs/RULINGS.md, *"the floor may not change a control's
     DRAWN size"*, and the owner's *"smaller controls are fine I hate the massive boxes"*. A
     44px-tall pair of text links would put a 44px band inside a card whose whole point is that
     it is quiet. So it takes `.btn`'s own overhang shape: the mark stays 18px, the TARGET is 44.
   ⚠ VERTICAL ONLY (`left:0;right:0`, height from the token). The two actions sit side by side
   with a `·` between them, and a horizontal halo would overhang the one next to it and steal its
   taps — the mis-tap the 8px TAP_GAP was invented to stop. `position:relative` sits on the base
   rule above, because an absolutely positioned ::after with no positioned ancestor anchors to
   the PAGE. */
.prof-steam-act{position:relative}
@media(pointer:coarse){
  .prof-steam-go{min-height:var(--tap-min)}
  .prof-steam-act::after{content:"";position:absolute;left:0;right:0;top:50%;
    transform:translateY(-50%);height:var(--tap-min)}
}
.prof-motto-form{display:flex;gap:8px;align-items:center;flex-wrap:wrap;width:100%}
.prof-motto-input{flex:1;min-width:220px;font-size:13px}
.prof-lineage{margin-top:10px;font-size:11px;color:var(--dim);letter-spacing:calc(.03em * var(--track))}
.prof-lin-sub{color:var(--faint)}

/* sections */
.prof-sec{margin-top:22px}
.prof-sec > .dsr-h{font-size:9.5px;letter-spacing:calc(.2em * var(--track));text-transform:var(--caps);color:var(--faint);
  border-bottom:1px solid var(--hair);padding-bottom:5px;margin-bottom:11px}
.prof-sec > .dsr-h::before{content:"◈ ";color:var(--accent-deep)}

/* reputation */
.prof-scorewrap{max-width:340px}
.prof-scorewrap .rail-score{font-size:34px}
.prof-scorewrap .rail-track{margin-top:6px}
/* record — reuse .dsr-stats but two columns on wide */
.prof-sec .dsr-stats{max-width:520px}

/* punish record (owner ruling 2026-07-20b) — last-90d penalties, admin ✕ pardon. */
.prof-record .rec-sub{margin:-2px 0 8px}
.rec-list{display:flex;flex-direction:column;gap:5px;max-width:520px}
.rec-row{display:flex;align-items:center;gap:9px;padding:5px 9px;border:1px solid var(--hair);
  border-left:2px solid var(--cat-nsfl);background:var(--sunk);font-size:12px}
.rec-date{color:var(--faint);font-size:10px;letter-spacing:calc(.06em * var(--track));white-space:nowrap;min-width:56px}
.rec-what{color:var(--dim);flex:1}
.rec-tag{color:var(--cat-nsfl)}
.rec-post{color:var(--accent)}
.rec-pardon{border:1px solid var(--hair-hi);background:none;color:var(--faint);cursor:pointer;
  width:22px;height:22px;line-height:1;border-radius:3px;flex:none}
.rec-pardon:hover{color:var(--good);border-color:var(--good)}
.rec-pardon:disabled{opacity:.4;cursor:default}

/* gallery */
.prof-pinned{margin-bottom:16px}
.prof-pinned-h{font-size:10px;letter-spacing:calc(.1em * var(--track));color:var(--accent);margin-bottom:7px}
/* RANK PINS (owner ruling 2026-07-20c): multiple pins in a small responsive row. */
.prof-pins{display:grid;grid-template-columns:repeat(auto-fill, minmax(150px, 1fr));gap:10px;align-items:start}
.prof-pinned-cell{max-width:260px}
.prof-grid{display:grid;grid-template-columns:repeat(auto-fill, minmax(170px, 1fr));gap:10px;align-items:start}
.prof-cell{margin:0;position:relative}
.prof-cell .tile{margin:0}
/* corner overflow — the set-pfp/pin actions stay hidden until tapped (no bar under
   every post). Always faintly visible so it's tappable on touch; the ⋯ becomes
   ★/📌 when the post is your pfp/pin. */
.prof-cell-menu{position:absolute;top:6px;right:6px;z-index:3;width:24px;height:24px;line-height:1;padding:0;
  display:flex;align-items:center;justify-content:center;font-size:12px;cursor:pointer;border-radius:3px;
  background:rgba(10,11,13,.72);border:1px solid var(--hair-hi);color:var(--ink);opacity:.55;transition:opacity .15s}
.prof-cell:hover .prof-cell-menu,.prof-cell-menu.on{opacity:1}
.prof-cell-menu.on{color:var(--accent);border-color:var(--accent-deep)}
.prof-cell-acts{position:absolute;left:6px;right:6px;bottom:6px;z-index:3;display:flex;gap:5px;flex-wrap:wrap;
  background:rgba(10,11,13,.9);padding:5px;border:1px solid var(--hair-hi);border-radius:3px}
.prof-cell-acts[hidden]{display:none}
.prof-cell-btn{font-size:8.5px;letter-spacing:calc(.1em * var(--track));text-transform:var(--caps);padding:3px 7px;
  border:1px solid var(--hair);color:var(--dim);background:var(--panel)}
.prof-cell-btn:hover{border-color:var(--accent);color:var(--accent)}
.prof-cell-btn.on{color:var(--accent);border-color:var(--accent-deep)}
/* ⛔ A COARSE POINTER GETS A REAL TARGET (forage-1wq2). These two draw ~21px tall, 5px apart,
   and they are reached ONLY by tapping the ⋯ on your own gallery cell — which app.css gives a
   44px halo to, i.e. it was deliberately made thumb-reachable. The thumb then lands on these.
   ⚠ AND THE CENSUS COULD NOT SEE THEM AT ALL. mobiletaps.test.js asked "does the rule say
   cursor:pointer, or does the SELECTOR name a button" — and these are real <button> elements
   styled by class, so they answered neither and were reported as NOTHING: not a pass, not a
   failure. The census reads the client's own el("button", "...") calls now, which is why this
   rule finally has to exist.
   ⚖ THE OVERHANG, NOT min-height — "the floor may not change what a member can see" (owner
   ruling 2026-08-20). The drawn chip stays 21px; only where a thumb lands changes.
   ⚠ VERTICAL ONLY, AND THE GAP DOES THE HORIZONTAL HALF: two buttons 5px apart mean a sideways
   halo reaches across and steals its neighbour's tap, which is the mis-tap TAP_GAP exists to
   stop. The row's gap goes to 8px on a coarse pointer instead. */
@media(pointer:coarse){
  .prof-cell-acts{gap:8px}
  .prof-cell-btn{position:relative}
  .prof-cell-btn::after{content:"";position:absolute;left:0;right:0;top:50%;
    transform:translateY(-50%);height:var(--tap-min)}
}
.prof-more{margin:14px auto;display:block}
.prof-empty{color:var(--faint);font-size:12px;padding:8px 0}
.prof-empty.hint{color:var(--faint)}

/* ===== PROFILE v2 — THE COMPLETE ACCOUNT PAGE (owner design D rev 2) ===== */
.prof-lbl{position:relative;z-index:3;font-size:10px;letter-spacing:calc(.16em * var(--track));text-transform:var(--caps);color:var(--faint);margin:0 0 8px}
/* 1 · the plaque */
.prof-plaque{display:flex;align-items:flex-end;gap:18px;padding:8px 0 18px;border-bottom:1px solid var(--hair);flex-wrap:wrap}
.prof-plaque .prof-avatar,.prof-plaque .prof-avatar-fallback{flex:0 0 auto}
.plaque-main{flex:1 1 260px;min-width:0}
.plaque-meta{color:var(--dim);font-size:11.5px;margin-top:3px;line-height:1.7}
.plaque-rank{font-weight:600;letter-spacing:calc(.03em * var(--track))}
/* ═══ FOUR SPECIES, FOUR SHAPES (owner ask 2026-08-08: "there has got to be a way to make this
   cleaner") ═══════════════════════════════════════════════════════════════════════════════════
   `.plaque-chips` + `.tchip.solid` ARE GONE, and the plaque was their only caller. That one pill
   dressed a MEASURE (trust, vote weight), an ACTIVITY reading (streak), a WARNING (flags) and
   the MARKS you hold (a class mark, medals) identically, so a glance could not sort them and the row
   had to be read item by item. Now: marks are bare icons (here), measures are a stat strip in
   the score column (below, beside `.plaque-score` — they belong with the number), and the
   warning is its own ruled line. Do not re-introduce a shared chip class across the three. */
.plaque-marks{display:flex;gap:8px;flex-wrap:wrap;align-items:center;margin-top:10px;min-width:0}
/* CLASS then RECORD, and this hairline is the seam between those two questions — NOT a
   separator between medals. It is the only furniture left in the row, on purpose. */
.marks-div{flex:0 0 auto;align-self:stretch;width:1px;min-height:20px;margin:0 3px;background:var(--hair-hi)}
/* THE MAINTENANCE LINKS (rename · edit motto). Quiet by construction: no button chrome, faint
   until hovered, and small enough that they cannot compete with the username above them. They
   keep a real focus ring — demoting a control's LOOK must never demote its reachability. */
.plaque-selfacts{display:flex;gap:14px;flex-wrap:wrap;align-items:baseline;margin-top:8px}
.plaque-link{background:none;border:0;border-bottom:1px dotted var(--hair-hi);padding:0 0 1px;
  font:inherit;font-size:10.5px;letter-spacing:calc(.08em * var(--track));text-transform:var(--caps);
  color:var(--faint);cursor:pointer}
.plaque-link:hover{color:var(--accent);border-bottom-color:var(--accent)}
.plaque-link:focus-visible{outline:1px solid var(--accent);outline-offset:3px}
/* ⚠ THE WARNING IS NOT A MEASURE AND NOT A MARK (see profPlaque). It is deliberately the only
   thing on the plaque wearing a solid left rule — a shape neither the stat strip nor the icon
   row can ever take, so it cannot be mistaken for either at a glance. A warning that looks like
   a statistic is a warning nobody reads. */
.plaque-warn{display:inline-flex;align-items:center;gap:7px;margin-top:9px;padding:4px 10px 4px 9px;
  font-size:11px;letter-spacing:calc(.04em * var(--track));color:var(--bad);
  border-left:3px solid var(--bad);background:rgba(255,77,94,.07)}
/* ===== THE MEDAL RAIL, ICON-FIRST (owner ask 2026-08-08) =====================================
   *"give the medals nice icons … hide the names until clicked or hovered"*. The `.mchip` text
   chip that lived here ("🌅 day one", one rule per rarity) is GONE: awardBadge() builds these
   now, so the medals on this page stopped being a second hand-rolled rendering of a thing that
   already had a builder — and the drawn AWARD_ICON glyphs can finally reach the screen.

   ⚖ WHY THE COMPACT FORM LIVES IN THIS FILE AND NOT app-rank.css. app-rank.css owns
   `.award-badge` — the 106px CABINET CARD, icon over title over percentage — and it still
   does; this is the same medal shrunk to a bare icon, sitting in the `.plaque-marks` row
   between the class mark and the score, both of which are declared right above. The base is
   REUSED, not copied, so the six rarity colours and the legendary/mythic shimmer arrive from
   app-rank.css untouched; only the box changes. `.medal-btn` raises specificity to (0,2,0), so
   it wins over `.award-badge` on merit rather than on the cascade order between two parts. */
.award-badge.medal-btn{width:auto;min-width:34px;height:34px;padding:4px;display:inline-flex;
  align-items:center;justify-content:center;cursor:pointer;font:inherit;border-radius:4px;
  overflow:hidden;transition:transform .12s ease,box-shadow .15s ease}
.award-badge.medal-btn .aw-icon{font-size:20px;filter:drop-shadow(0 0 5px currentColor)}
.award-badge.medal-btn .aw-icon.aw-drawn{height:22px}
.award-badge.medal-btn .aw-icon .awi,
.award-badge.medal-btn .aw-icon .clsi{width:22px;height:22px}
/* ⚠ THE DWELL FILL PAINTS UNDER THE ICON, and this rule is the only thing making that true.
   `.tagfill` (app-tagrows.css) is absolutely positioned and appended LAST, so against a
   static sibling it wins on paint order regardless of its z-index:0 — the bar would crawl
   across the FRONT of the medal. app-tagrows.css lifts a tag chip's children for exactly this
   reason and its selector list does not reach here; `:not(.tagfill)` is copied with it because
   forcing position:relative onto the bar makes it an inline element that `width` cannot size,
   which is the bug written up beside that rule. */
.award-badge.medal-btn>*:not(.tagfill){position:relative;z-index:1}
.award-badge.medal-btn:hover,.award-badge.medal-btn:focus-visible{transform:translateY(-1px)}
.award-badge.medal-btn:focus-visible{outline:1px solid currentColor;outline-offset:2px}
/* ⚖ 44px UNDER A THUMB. Most viewers are on phones and this is the archive's own target law
   (app-tagrows.css writes it for the popup's close control). It is a COARSE-POINTER rule and
   not a width breakpoint: a small window on a desktop still has a mouse, and inflating the
   chips there would push the score off the plaque for no one's benefit. */
@media(pointer:coarse){
  .award-badge.medal-btn{min-width:44px;height:44px}
  .award-badge.medal-btn .aw-icon.aw-drawn{height:26px}
  .award-badge.medal-btn .aw-icon .awi,
  .award-badge.medal-btn .aw-icon .clsi{width:26px;height:26px}
}
@media(prefers-reduced-motion:reduce){.award-badge.medal-btn{transition:none}
  .award-badge.medal-btn:hover,.award-badge.medal-btn:focus-visible{transform:none}}
/* THE CLASS MARKS (scion · binkie) — deliberately NOT a rarity band. Neither carries a
   `.rarity-*` class at all, so both inherit `.award-badge`'s neutral hairline and take the
   colour the rest of the app already paints that class's names with. Nothing here glows: a glow
   is what the rarity tiers mean, and a class has no tier.
   ⚖ THE SHAPE IS SHARED, THE HUE IS NOT. `.class-mark` holds everything that says "this is a
   class, not a medal" (dashed hairline, no glow, panel ground) and `.mark-<kind>` holds only
   the colour — so the two marks are the same species at a glance and a third class would be one
   line, not a second treatment. The base keeps a fallback colour so a mark is never invisible
   if its var goes missing. */
.award-badge.medal-btn.class-mark{color:var(--dim);
  border-color:var(--hair-hi);border-style:dashed;box-shadow:none;background:var(--panel)}
.award-badge.medal-btn.class-mark.mark-scion{color:var(--scion);border-color:var(--scion)}
.award-badge.medal-btn.class-mark.mark-binkie{color:var(--binkie);border-color:var(--binkie)}
.award-badge.medal-btn.class-mark .aw-icon{filter:none}

/* ===== THE MEDAL / CLASS DETAIL CARD — the RICH TAG CARD's shell, a different body ==========
   `.tagcard` (app-tagrows.css) owns the frame, the clamp, the scroll and the popup form; these
   rules only say what a medal body looks like inside it. The card's border is
   `1px solid currentColor`, so setting `color` here IS setting the frame to the rarity band. */
.medalcard{color:var(--r-common)}
.medalcard.rarity-uncommon{color:var(--r-uncommon)}
.medalcard.rarity-rare{color:var(--r-rare)}
.medalcard.rarity-epic{color:var(--r-epic)}
.medalcard.rarity-legendary{color:var(--r-legendary)}
.medalcard.rarity-mythic{color:var(--r-mythic)}
.medalcard.scioncard{color:var(--scion,var(--dim))}
.medalcard.binkiecard{color:var(--binkie,var(--dim))}
/* LIGHT-SKIN CORRECTION, the same one app-rank.css writes for `.scion-name` / `.binkie-name`:
   both hues are dark-UI colours and wash out on a light page. THE MARK AND ITS CARD TAKE THE
   SAME CORRECTION, or a class reads as one colour on the plaque and another inside the card it
   opens. */
html[data-light="1"] .award-badge.medal-btn.class-mark.mark-scion,
html[data-light="1"] .medalcard.scioncard{color:var(--scion-lo);border-color:var(--scion-lo)}
html[data-light="1"] .award-badge.medal-btn.class-mark.mark-binkie,
html[data-light="1"] .medalcard.binkiecard{color:var(--binkie-lo);border-color:var(--binkie-lo)}
.medalcard .tagcard-name{color:var(--bright)}
/* The rarity sits where the tag card's post COUNT sits — same slot, same weight, and it is
   text rather than a link because a medal has nothing to search for. */
.medalcard-pct{margin-left:auto;font-size:10.5px;color:currentColor;white-space:nowrap}
.medalcard .crownpanel{margin-top:10px;padding-top:9px;border-top:1px solid var(--hair)}
.medalcard .cp-v{line-height:1.55}
.plaque-score{text-align:right;flex:0 0 auto;margin-left:auto}
.plaque-scoreval{font-size:40px;line-height:1;color:var(--bright);font-variant-numeric:tabular-nums}
/* THE MEASURES — trust · weight · streak, the same species as the number they sit under, which
   is the whole reason they moved out of the marks row. Tabular figures so the column of digits
   lines up; the label is the smaller, fainter half so the VALUE is what the eye lands on. */
.plaque-stats{display:flex;flex-wrap:wrap;justify-content:flex-end;gap:4px 14px;margin-top:7px}
.pstat{display:inline-flex;align-items:baseline;gap:5px;white-space:nowrap}
.pstat-k{font-size:9px;letter-spacing:calc(.16em * var(--track));text-transform:var(--caps);color:var(--faint)}
.pstat-v{font-size:12.5px;color:var(--ink);font-variant-numeric:tabular-nums}
/* the streak keeps its warm colour — it is the one figure here that DECAYS, so it is the one
   worth catching out of the corner of an eye. */
.pstat.streak .pstat-k,.pstat.streak .pstat-v{color:var(--accent)}
/* 2 · the FULL-WIDTH score spine */
/* THREE SEPARATE gradient bars (owner ruling 2026-07-21h) — each bar its own
   thematic palette: content=fire, categorization=ocean, community=bloom. */
.prof-spine{margin-top:16px;display:flex;flex-direction:column;gap:10px}
.spine-row{display:flex;flex-direction:column;gap:4px}
.spine-head{display:flex;justify-content:space-between;align-items:baseline}
.spine-lbl{font-size:10px;letter-spacing:calc(.14em * var(--track));text-transform:var(--caps);color:var(--dim)}
.spine-pct{font-size:11px;color:var(--bright)}
.spine-track{height:10px;border:1px solid var(--hair);border-radius:5px;overflow:hidden;width:100%;background:var(--sunk)}
.spine-fill{height:100%;border-radius:4px;transition:width .3s ease}
/* CONTENT — fire: red -> orange -> yellow */
.spine-row.sp-content .spine-lbl{color:#ff8a3d}
.spine-row.sp-content .spine-fill{background:linear-gradient(90deg,#e0393f,#f2792b,#ffd23f)}
/* CATEGORIZATION — ocean: indigo -> blue -> cyan */
.spine-row.sp-categorization .spine-lbl{color:#4fc3e0}
.spine-row.sp-categorization .spine-fill{background:linear-gradient(90deg,#4a4fd8,#3fa7e0,#4fe0d8)}
/* COMMUNITY — bloom: magenta -> violet -> blue-violet */
.spine-row.sp-community .spine-lbl{color:#d878d0}
.spine-row.sp-community .spine-fill{background:linear-gradient(90deg,#e0479f,#a25fe0,#6a7fe0)}
/* PENALTIES (owner ruling 2026-07-30b) — crowd punishment taken off the TOTAL, so it is
   NOT one of the three bars and deliberately does not share their palette: ash red, the
   only row here that is a subtraction. Rendered only when there is one. */
.spine-row.sp-penalty .spine-lbl{color:#e0555f}
.spine-row.sp-penalty .spine-add{color:#e0555f}
.spine-row.sp-penalty .spine-fill{background:linear-gradient(90deg,#7a1c22,#e0555f)}
/* STATS-tab detailed breakdown (owner ruling 2026-07-21h) — same three accent
   colors as the Main-tab bars, as a left rail instead of a gradient fill. */
.pillar-detail{border-left:3px solid var(--hair-hi);padding-left:12px}
.pillar-detail.sp-content{border-left-color:#f2792b}
.pillar-detail.sp-content .prof-lbl{color:#ff8a3d}
.pillar-detail.sp-categorization{border-left-color:#3fa7e0}
.pillar-detail.sp-categorization .prof-lbl{color:#4fc3e0}
.pillar-detail.sp-community{border-left-color:#a25fe0}
.pillar-detail.sp-community .prof-lbl{color:#d878d0}
/* 3 · the crowns — title WALL (wrapping grid, never a scroller) */
.crown-wall{display:grid;grid-template-columns:repeat(auto-fill, minmax(128px, 1fr));gap:12px}
.crown-box{position:relative;border:1px dashed var(--hair-hi);background:var(--panel);padding:10px 8px 9px;
  text-align:center;border-radius:6px;isolation:isolate}
.crown-box.tier-common{border-color:rgba(154,161,173,.5);color:var(--r-common)}
.crown-box.tier-uncommon{border-color:rgba(95,196,107,.5);color:var(--r-uncommon)}
.crown-box.tier-rare{border-color:rgba(77,184,255,.55);color:var(--r-rare);box-shadow:0 0 12px rgba(77,184,255,.1)}
.crown-box.tier-epic{border-color:rgba(217,102,232,.55);color:var(--r-epic);box-shadow:0 0 12px rgba(217,102,232,.12)}
.crown-box.tier-legendary{border-color:rgba(255,217,138,.6);color:var(--r-legendary);box-shadow:0 0 14px rgba(255,217,138,.14)}
.crown-box.tier-mythic{border-color:rgba(255,77,94,.6);color:var(--r-mythic);box-shadow:0 0 14px rgba(255,77,94,.16)}
.crown-box.apex-frame{border:1px solid transparent;color:#e8ecff;background:var(--panel)}
/* ═══ ⚰ REPEALED 2026-09-08 — THE CROWN WALL'S MARKS ARE **NOT** IN THE PALETTE ══════════════
   ⛔ EVERYTHING BETWEEN HERE AND THE NEXT BLOCK IS THE SUPERSEDED ARGUMENT, kept because it names
   what the filter list was FOR and in what order, and deleted reasoning is how this repo loses the
   argument behind a decision. It is NOT a licence to re-apply any of it. The owner ruled, verbatim,
   *"the crowns are special, they have a unique theme."* — the filter chain went with `e50045c8` on
   both surfaces it had reached, and `contrast(.85)` was the real loss: it flattened the vendor's
   moulded shading, which is exactly the prismatic read he is protecting. The standing ruling is the
   paragraph in app-screens.css above `.pns-lb-crown .roster-title`, and `cornercell_e2e.js` §3 is
   the gate that reddens on a re-tint. ⚠ Read the paragraph below as history, in the past tense.

   THE SUPERSEDED ARGUMENT (forage-qw2w.36, second surface), as written:
   The debt named in app-screens.css:337 and in forage-qw2w.36's own commit — *"Not done: the
   profile crown wall (crownBox, common-profile.js) still paints the vendor's colours"* — paid on
   the same terms as /leaderboard, because it is the same defect on another screen.
   ⚖ THE MARKS ARE NOT REPLACED, AND THAT IS THE OWNER'S RULING, NOT A SHORTCUT: *"the crowns are
   the owner's own emoji marks (crowns spec); replacing them with drawn glyphs is a design he must
   choose."* So `TITLE_META`'s subjects (common-profile.js) are byte-identical and only the PAINT
   changes — the SAME six-function recipe /leaderboard uses, so the two screens cannot drift.
   ⚠ THE ORDER OF THE FILTER LIST IS THE WHOLE DESIGN. The recolour runs FIRST and `drop-shadow`
   LAST, so the halo is still drawn in `currentColor` — which on this surface is the TIER colour
   (`--r-common` … `--r-mythic`, and `#e8ecff` under `.apex-frame`). Put the recolour after the
   shadow and the halo turns amber too, and the wall loses the one channel that says how rare a
   crown is. The tier still speaks in three places: the border, the name and this halo.
   ⚠ MEASURED AT THE 19px IT SHIPS AT here, not at /leaderboard's 17px — a filter that reads as
   flat at one size can read as muddy at another, so this was photographed on the real wall.
   ⛔ ON THE GLYPH, NEVER ON `.crown-box`. A filter creates a containing block for fixed and
   absolute descendants, and the box holds five of them (`.prismring`, `.prismglow`, two
   `.corner`s and `.aw-burst`) — the apex frame would collapse into its own cell.
   ⚑ `.apex-crown` (the 👑 before a name, app-rank.css:681) is the SAME vendor glyph on a third
   surface and is NOT fixed here: it is one mark rather than a wall, it lands inside a username's
   own colour rules, and it is not this lane's. Named, not touched. */
/* ⛔ NO TINT — the crowns are exempt from the palette (owner ruling 2026-09-08); see the
   paragraph in app-screens.css. The halo stays: it is the tier channel, not a repaint. */
.crown-g{font-size:19px;line-height:1;filter:drop-shadow(0 0 6px currentColor)}
.crown-n{margin-top:7px;font-size:11.5px;letter-spacing:calc(.04em * var(--track));font-weight:600;color:currentColor}
.crown-t{margin-top:4px;font-size:9.5px;letter-spacing:calc(.06em * var(--track));color:var(--faint);text-transform:var(--caps)}
.crown-box.tier-common .crown-t,.crown-box.tier-uncommon .crown-t,.crown-box.tier-rare .crown-t,
.crown-box.tier-epic .crown-t,.crown-box.tier-legendary .crown-t,.crown-box.tier-mythic .crown-t{color:currentColor;opacity:.85}
.title-unlock{animation:aw-pop .5s ease}
/* 5 · the pin wall */
.pin-grid{display:grid;grid-template-columns:repeat(auto-fill, minmax(150px, 1fr));gap:10px;align-items:start}
.pin-cell{position:relative}
.pin-cell .tile img{width:100%;height:auto;display:block;aspect-ratio:1 / 1.1;object-fit:cover;object-position:top;border:1px solid var(--hair-hi)}
.pin-unpin{position:absolute;top:5px;right:5px;width:22px;height:22px;border:1px solid var(--hair-hi);
  background:rgba(9,10,12,.78);color:var(--dim);cursor:pointer;border-radius:3px;line-height:1;z-index:2}
.pin-unpin:hover{color:var(--bad);border-color:var(--bad)}
/* 6 · the works */
.works-head{display:flex;align-items:baseline;gap:12px;margin-bottom:8px;flex-wrap:wrap}
.works-head .prof-lbl{position:relative;z-index:3;margin:0}
.works-count{color:var(--dim);font-size:11px;letter-spacing:calc(.06em * var(--track))}
.works-all{margin-left:auto;color:var(--accent);font-size:11px;letter-spacing:calc(.08em * var(--track));text-decoration:none}
.works-all:hover{color:var(--bright)}
/* ⚠ THE PLAQUE'S 640px RULES ARE ALL IN ONE BLOCK, further down this file — see "MOBILE PLAQUE
   … rev 4". A second block lived here and it had gone STALE: it opened `.prof-plaque` as "a
   wrapping flex row" and forced the score onto its own line with `flex:1 0 100%`, both true of
   rev 2 and neither true since rev 3 made the narrow plaque a GRID. A flex shorthand on a grid
   item is ignored, so that declaration did nothing for two revisions while plaque.test.js
   asserted it — a gate can only measure the rule it was pointed at, and this one was pointed at
   a dead one. Its live declarations (the 32px score, the strip's gaps) moved into the rev 4
   block; nothing here was deleted without being carried. */

/* top tags */
.prof-tags{display:flex;flex-wrap:wrap;gap:7px}
.prof-tag{display:inline-flex;align-items:center;gap:7px;font-size:11px;letter-spacing:calc(.04em * var(--track));
  padding:3px 9px;border:1px solid var(--hair-hi);color:var(--accent);position:relative}
.prof-tag:hover{border-color:var(--accent);color:var(--bright)}
/* ⚖ 25px OF CHIP, 44px OF TARGET — AND THE GAP HAD TO MOVE FIRST (forage-2xcc, 2026-08-25).
   MEASURED in the real app at 390x844 with touch, /u/evan?tab=stats with the `top tags` group
   open: the chips are **57-109 x 25**, wrapped over three rows, **7px** apart in BOTH axes.
   ⛔ AT gap:7px THE OVERHANG IS A DEFECT, NOT A FIX. 44px around a 25px chip is 9.5px each way,
   which is 2.5px MORE than the row gap — so the lower row's halo covers the bottom 2.5px of the
   chip ABOVE it and, being later in the DOM, wins the hit test there. That is the halo stealing
   a visible control's taps, which is the one thing this shape is not allowed to do.
   ⚖ SO THE ROW GAP GOES TO 10px ON A COARSE POINTER and the halos meet edge-to-edge instead of
   overlapping. 3px of extra space on a wrapped chip cloud, on touch only, is the cheapest
   currency on this screen — the alternative is 19px of gap to make the halos tile at 7px of
   drawn separation, which would be a redesign of the group.
   ⚠ VERTICAL ONLY, for the usual reason: sideways the same 7px separates two tag searches. */
@media(pointer:coarse){
  .prof-tags{gap:10px}
  .prof-tag::after{content:"";position:absolute;left:0;right:0;top:50%;
    transform:translateY(-50%);height:var(--tap-min)}
}
.prof-tag-c{color:var(--faint);font-variant-numeric:tabular-nums}

/* PROFILE TABS (owner ruling 2026-07-20 iFunny tabs) — the tab BUTTONS reuse the
   .sortopt terminal look (see .sortbar); this is only the row layout + content shell. */
.prof-tabs{display:flex;gap:4px;flex-wrap:wrap;margin:16px 0 14px;border-bottom:1px solid var(--hair);padding-bottom:12px}
.prof-tabcontent{display:block}
/* STATS tab — the statRow dossier idiom laid out two-up on wide viewports. */
.prof-statlist{display:grid;grid-template-columns:1fr 1fr;gap:4px 26px}
.prof-statlist .dsr-stat{padding:3px 0;border-bottom:1px solid var(--hair)}
@media(max-width:640px){.prof-statlist{grid-template-columns:1fr}}
.prof-foot{min-height:1px}

/* ═══════════════ THE STATS TAB, REBUILT (owner ask 2026-08-08) ═══════════════════════════
   *"redesign the stats page to contain all this new data … don't forget to make it look nice
   … so that we can add descriptions or dropdowns underneath the stats explaining and showing
   what they are and how they work."* Built by web/common-stats.js; gated by
   server/tests/statspage.test.js; intent in docs/surfaces/profile.md.

   ⚖ MOBILE FIRST, AND EVERY RULE HERE IS THE NARROW ONE. The defaults below ARE the phone
   layout — one column, one group open, everything else a 44px header you tap. The only
   breakpoint is `min-width:641px`, which does one thing: puts the grid in two columns because
   there is room. That is the opposite of the rest of this file, which is desktop-first with
   max-width:640 overrides, and it is deliberate — a forty-stat page is a phone problem first.

   ⚖ ONE SHAPE PER SPECIES (docs/SPEC_profiles.md §1, the plaque's lesson). Five kinds of
   number, five treatments, and the LEFT STRIPE is the species said in colour so the kind is
   readable before the value is: count hairline · measure amber · rate blue · time warm ·
   standing violet. A count is monospace and right-aligned because counts get compared to each
   other; a time is text and NOT in the numeric column, because lining "seen 8h ago" up under
   "1,204" is exactly what made the old flat list unreadable. */
.statgrp{border:1px solid var(--hair);background:var(--sunk);margin-top:12px}
.statgrp-h{display:flex;align-items:center;gap:10px;padding:12px 13px;min-height:44px;
  cursor:pointer;list-style:none;user-select:none}
.statgrp-h::-webkit-details-marker{display:none}   /* the +/− below is the marker */
.statgrp-h::after{content:"+";margin-left:auto;color:var(--faint);font-size:15px;line-height:1}
.statgrp[open] > .statgrp-h::after{content:"\2212"}
.statgrp-h:hover{background:var(--panel)}
.statgrp-h .statgrp-lbl{margin:0}
.statgrp-n{color:var(--faint);font-size:10px;font-variant-numeric:tabular-nums}
.statgrp-lock{font-size:9px;letter-spacing:calc(.1em * var(--track));text-transform:var(--caps);
  color:var(--info);border:1px solid var(--info);padding:1px 5px}
.statgrp-note{padding:0 13px 11px;font-size:11px;line-height:1.5;color:var(--dim)}
.statgrp-admin{border-color:var(--info)}
.statgrp-ledger .prof-sec{margin:0;padding:0 13px 12px}

/* THE GRID — one column on a phone, two where there is room. The 1px gap over a --hair
   background is the hairline between cells; no per-cell border to double up at the seams. */
.statgrid{display:grid;grid-template-columns:1fr;gap:1px;background:var(--hair);
  border-top:1px solid var(--hair)}
@media(min-width:641px){.statgrid{grid-template-columns:1fr 1fr}}

.statcell{display:grid;grid-template-columns:1fr auto;grid-auto-rows:min-content;
  align-items:baseline;gap:3px 12px;width:100%;padding:10px 13px 11px;text-align:left;
  background:var(--sunk);border:0;border-left:2px solid var(--hair-hi);
  font:inherit;color:var(--ink);cursor:default}
.statcell-on{cursor:pointer}
.statcell-on:hover,.statcell-on:focus-visible{background:var(--panel);outline:none}
.statcell-on:focus-visible{border-left-color:var(--accent)}
/* ⚠ THE DWELL FILL PAINTS UNDER THE ROW, and this rule is the only thing making that true —
   the SAME defect `.award-badge.medal-btn` documents and fixes above, which was never carried
   to the stat rows. Every armed `.statcell` is handed to armCardOn → armInfoCard, which
   APPENDS a `.tagfill` (app-tagrows.css) as the last child and sets `position:relative` on the
   host if it is static. An absolutely-positioned element paints above every static in-flow
   sibling regardless of its `z-index:0`, so without this the fill bar crawled across the FRONT
   of the label, the value and the band on every stat that explains itself — which is all of
   them. `:not(.tagfill)` is copied with the rule because forcing `position:relative` onto the
   bar makes it an inline element that `width` cannot size; that is written up beside the rule
   in app-tagrows.css and it is the bug that made the bar compute to 0px and never move.
   ⚠ AND `.statcell` IS A GRID, so the fill is also a grid ITEM. It stays out of the track flow
   only because it is absolutely positioned — never give it a `grid-` placement. */
.statcell>*:not(.tagfill){position:relative;z-index:1}
.statcell-k{grid-column:1;grid-row:1;font-size:10px;color:var(--faint);
  letter-spacing:calc(.1em * var(--track));text-transform:var(--caps)}
.statcell-v{grid-column:2;grid-row:1;justify-self:end;font-size:15px;color:var(--bright)}
.statcell-sub{grid-column:1;grid-row:2;font-size:10.5px;color:var(--dim);line-height:1.4}
/* THE BAND CHIP — the SAME six-band ladder the crowns and medals use (--r-* in app-rank.css),
   at chip scale. ⚠ `.tier-*` is scoped to `.crown-box`/`.title-chip` over there, so these
   rules are not optional decoration: without them a band renders as unstyled grey text, which
   is the "an icon with no CSS box" shape this feature has already shipped once. */
.statcell .statband{grid-column:2;grid-row:2;justify-self:end;font-size:9px;padding:1px 5px;
  border:1px dashed currentColor;letter-spacing:calc(.06em * var(--track));white-space:nowrap;
  color:var(--r-common)}
.statband.tier-uncommon{color:var(--r-uncommon)}
.statband.tier-rare{color:var(--r-rare)}
.statband.tier-epic{color:var(--r-epic)}
.statband.tier-legendary{color:var(--r-legendary)}
.statband.tier-mythic{color:var(--r-mythic)}
/* apex: a SOLID hairline in the pastel the apex chip settles on, not a sheet — see the note
   in common-stats.js statCell about SHEETED. */
.statband.tier-apex{color:#e8ecff;border:1px solid #d4f3ff}
.statmeter{grid-column:1 / -1;grid-row:3;height:3px;background:var(--hair);margin-top:6px}
.statmeter-f{display:block;height:100%;background:var(--accent)}

/* THE FIVE SPECIES. */
.statcell.sk-count{border-left-color:var(--hair-hi)}
.statcell.sk-count .statcell-v{font-family:var(--mono);font-size:14px;color:var(--ink)}
.statcell.sk-measure{border-left-color:var(--accent)}
.statcell.sk-measure .statcell-v{font-size:19px;color:var(--bright)}
.statcell.sk-rate{border-left-color:var(--info)}
.statcell.sk-rate .statcell-v{font-family:var(--mono);font-size:14px;color:var(--info)}
.statcell.sk-time{border-left-color:#d09a3c}
.statcell.sk-time .statcell-v{font-size:12px;color:var(--ink);letter-spacing:0;
  font-variant-numeric:normal;justify-self:end;text-align:right}
.statcell.sk-standing{border-left-color:#a98bff}
.statcell.sk-standing .statcell-v{font-size:11px;color:var(--ink);border:1px solid var(--hair-hi);
  padding:2px 7px;letter-spacing:calc(.06em * var(--track))}
/* TONE rides on top of the species, never instead of it: a bad measure is still a measure. */
.statcell.tone-good .statcell-v{color:var(--good)}
.statcell.tone-bad{border-left-color:var(--bad)}
.statcell.tone-bad .statcell-v{color:var(--bad)}
.statcell.tone-bad.sk-standing .statcell-v{border-color:var(--bad)}
.statcell.tone-warm .statcell-v{color:var(--accent)}
/* ⚠ THIS REPO'S THUMB LAW (same block as .award-badge.medal-btn above): a control a finger
   uses is 44px tall, and a stat row IS a control — it opens the card that explains it. */
@media(pointer:coarse){.statcell{min-height:44px}}

/* THE HAND-OFF to the full account dossier on /admin. ⚠ THE IP TABLES ARE NOT DRAWN ON THIS
   SURFACE — web/page-admin-account.js owns them, over the one declared call site of that staff
   control. This is a sentence and a link, so it is styled as copy. */
.statdossier{border-top:1px solid var(--hair);padding-top:11px}
.statdossier-a{color:var(--info)}
.statdossier-a:hover{color:var(--bright);text-decoration:underline}

/* THE CARD BODY — the SAME shell as the medal and crown cards (.tagcard/.crownpanel in
   app-rank.css); only the value chip on the head is new, so a stat, a medal and a title
   explain themselves in one visual language rather than three. */
.statcard .statcard-v{margin-left:auto;color:var(--bright);font-size:13px}
.statcard.sk-measure{border-left:2px solid var(--accent)}
.statcard.sk-count{border-left:2px solid var(--hair-hi)}
.statcard.sk-rate{border-left:2px solid var(--info)}
.statcard.sk-time{border-left:2px solid #d09a3c}
.statcard.sk-standing{border-left:2px solid #a98bff}

/* Sub-headings inside a group (the histograms use them), and the upload-time charts. */
.statgrp-sub{padding:13px 13px 5px;font-size:9.5px;color:var(--faint);
  letter-spacing:calc(.16em * var(--track));text-transform:var(--caps)}
.statgrp-sub::before{content:"◈ ";color:var(--accent-deep)}
.statfoot{margin-top:14px;display:flex;flex-direction:column;gap:8px}
.statfoot-l{font-size:10.5px;line-height:1.5;color:var(--faint);border-left:1px solid var(--hair);
  padding-left:10px}

/* THE HISTOGRAMS. 24 bars have to fit a 320px phone, so a bar is a flex child with no minimum
   and the row never scrolls — a chart that scrolls sideways stops being a SHAPE, which is the
   only thing it is for at this size. Labels only where there are seven of them. */
.stathist{padding:0 13px 14px}
.stathist-bars{display:flex;align-items:flex-end;gap:2px;height:56px;margin-top:8px}
.stathist-bars.labeled{margin-bottom:16px}
.stathist-c{flex:1 1 0;min-width:0;display:flex;flex-direction:column;justify-content:flex-end;
  align-items:stretch;height:100%;position:relative}
.stathist-b{display:block;width:100%;background:var(--accent);opacity:.8}
.stathist-l{position:absolute;bottom:-14px;left:0;right:0;text-align:center;font-size:8px;
  color:var(--faint);letter-spacing:0}

/* THE TOP-VIEWED LISTS (the capture layer's only rows-with-a-post-behind-them).
   ⚖ A ROW, NOT A TILE. These sit inside a stats panel of eighty numbers, and a strip of
   thumbnails would make the one section that is about POSTS shout over every section that is
   about the account. The rank, the title and the count are a three-column grid that collapses
   to two on a phone — the count keeps its own column at every width, because comparing the
   numbers down the column is the only reason the list is ordered.
   ⚠ 44px UNDER A THUMB: each row is a LINK to a post, which is this repo's thumb law (the same
   block as .award-badge.medal-btn and .statcell). */
.stattop{padding:0 13px 12px}
.stattop-l{display:flex;flex-direction:column;gap:1px;margin-top:6px;background:var(--hair)}
.stattop-r{display:grid;grid-template-columns:auto 1fr auto;align-items:baseline;gap:2px 10px;
  padding:8px 11px;background:var(--sunk);color:var(--ink);text-decoration:none;min-height:38px}
.stattop-r:hover,.stattop-r:focus-visible{background:var(--panel);outline:none}
.stattop-r:focus-visible .stattop-t{text-decoration:underline}
.stattop-rank{grid-column:1;grid-row:1;font-size:10px;color:var(--faint)}
.stattop-t{grid-column:2;grid-row:1;font-size:12px;color:var(--bright);overflow:hidden;
  text-overflow:ellipsis;white-space:nowrap}
.stattop-n{grid-column:3;grid-row:1;justify-self:end;font-family:var(--mono);font-size:13px;
  color:var(--accent)}
.stattop-s{grid-column:2 / -1;grid-row:2;font-size:10px;color:var(--dim);line-height:1.4}
@media(pointer:coarse){.stattop-r{min-height:44px}}
/* INVITES tab — public direct-invitee roster + (self-only) minted keys. */
.inv-by{font-size:12px;color:var(--dim);margin-bottom:14px}
.inv-list{display:flex;flex-direction:column;gap:2px}
.inv-row{display:grid;grid-template-columns:1fr auto auto auto;align-items:baseline;gap:12px;
  padding:6px 0;border-bottom:1px solid var(--hair);font-size:12px}
.inv-rank{font-size:10px;letter-spacing:calc(.08em * var(--track));text-transform:var(--caps)}
.inv-score{color:var(--ink);font-variant-numeric:tabular-nums}
.inv-joined{color:var(--faint);font-variant-numeric:tabular-nums}
.inv-codes{display:flex;flex-direction:column;gap:2px;margin-top:12px}
.inv-code-row{display:grid;grid-template-columns:auto auto 1fr;align-items:baseline;gap:14px;
  padding:6px 0;border-bottom:1px solid var(--hair);font-size:12px}
.inv-code{font-family:var(--mono,ui-monospace,monospace);color:var(--accent);letter-spacing:calc(.06em * var(--track))}
.inv-code-row.used .inv-code{color:var(--faint);text-decoration:line-through}
.inv-code-date{color:var(--faint);font-variant-numeric:tabular-nums}
.inv-code-status{color:var(--dim)}
.inv-code-row.unused .inv-code-status{color:var(--info)}
/* the SHARE row on an UNUSED key (2026-08-31): the full /i/ link + copy, spanning the grid —
   the same readonly `.inv-link` idiom as /admin's invite table (app-builder.css, loaded by
   user.html too), because navigator.clipboard is missing on the plain-http LAN and a
   tap-to-select box is the manual path that always works. min-width:0 lets the box shrink
   inside the flex row instead of forcing a 390px phone to scroll sideways. */
.inv-code-share{grid-column:1/-1;display:flex;align-items:center;gap:6px;margin:2px 0 3px}
.inv-code-share .inv-link{flex:1 1 auto;width:auto;max-width:340px;min-width:0}
@media(max-width:640px){.inv-row{grid-template-columns:1fr auto;row-gap:2px}.inv-code-row{grid-template-columns:1fr}}

/* admin-only moderation controls */
.prof-mod{border:1px solid var(--hair-hi);background:var(--sunk);padding:14px 16px}
.prof-mod-note{font-size:10px;color:var(--info);letter-spacing:calc(.04em * var(--track));margin-bottom:9px}
.prof-mod-liab{font-size:11px;color:var(--dim);margin-bottom:11px}
.prof-mod-liab .warn{color:var(--bad)}
.prof-mod-acts{display:flex;flex-wrap:wrap;gap:8px;align-items:center}
.prof-mod-acts .btn.danger{color:var(--bad);border-color:var(--bad)}
.prof-mod-acts .btn.danger:hover{background:var(--bad);color:var(--void)}

@media(max-width:640px){
  .prof-name{font-size:21px}
}

/* MOBILE PLAQUE (owner ruling 2026-07-20; rev 4, owner report 2026-08-09: *"it's rather
   disorganized"*). Desktop untouched — every rule here is inside the 640px block.

   ⚠ WHAT REV 3 GOT WRONG, and it is worth naming because the areas LOOK right in the source.
   `"avatar sigil" / "avatar main" / "score score"` puts `.plaque-main` in the right-hand column
   FOR ITS WHOLE HEIGHT — and main is the motto, the maintenance links and the medal row as well
   as the identity, so on a phone it is three times the avatar's height. The avatar spans that
   row, fills 92px of it, and leaves a ~200px EMPTY RECTANGLE underneath itself. Worse, it gave
   the plaque TWO LEFT EDGES: everything in main started at 104px while `.plaque-score` — the
   biggest thing on the screen — started at 0. That misalignment is what reads as disorganised;
   the individual pieces were fine.

   ⚖ SO ONLY THE IDENTITY SITS BESIDE THE AVATAR, AND EVERYTHING ELSE IS FULL WIDTH — one left
   edge for the whole plaque. The obstacle is that `.plaque-main` is ONE element wrapping both
   the identity and everything under it, and splitting it in the builder would change a DOM that
   plaque.test.js drives. `display:contents` dissolves the wrapper's BOX without touching the
   tree, so its children become grid items in their own right and can be placed individually.
   ⚠ Which is why the placement below is written as "everything spans, then three exceptions" —
   the children of main are grandchildren of the grid, so `.prof-plaque > *` cannot reach them
   and a rule per child would silently miss the next one added (the motto form, the gag title).
   ⚠ AND WHY `.plaque-main`'s own box properties stop applying here: `flex:1 1 260px` and its
   min-width are inert under display:contents. That is intended — they are the desktop flex
   row's, and this is not it. */
.plaque-sigil{display:none}
@media(max-width:640px){
  .prof-plaque{
    display:grid;
    grid-template-columns:84px minmax(0,1fr);
    column-gap:12px;row-gap:0;
    align-items:start;
  }
  .plaque-main{display:contents}
  /* the default: a full-width row, in DOM order. Three items opt out, below. */
  .prof-plaque > *,.plaque-main > *{grid-column:1/-1;min-width:0}
  .prof-plaque .prof-avatar,.prof-plaque .prof-avatar-fallback{
    grid-column:1;grid-row:1/span 2;width:84px;height:84px;align-self:start}
  .plaque-sigil{display:flex;flex-direction:row;align-items:center;gap:6px;
    grid-column:2;grid-row:1;align-self:start;margin-bottom:4px}
  .plaque-sigil .sig-badge{width:24px;height:24px;font-size:11px;border-width:1px}
  /* the rank NAME beside the medallion (owner 2026-08-09). `.sig-name`'s base is the /profile
     crest's 18px — far too loud against a 24px ring — so it takes the rail's compact size. The
     tier colour and glow come from the rk-<rank> class paintSigil put on it. */
  .plaque-sigil .sig-name{font-size:11px;letter-spacing:calc(.14em * var(--track))}
  /* ...and the meta line drops it, separator included, so the tier is said once. */
  .plaque-rankwrap{display:none}
  /* row 2 is the name and its badges. Sigil + this is ~80px against an 84px avatar, so the
     two columns finish level; a name long enough to take a third line grows the block by one
     line rather than opening a hole the height of the page. */
  .prof-namerow{grid-column:2;grid-row:2;gap:6px 8px;align-items:center}
  /* ⚠ THE TIER IS SAID ONCE HERE TOO. `.plaque-sigil` exists ONLY at this width and its whole
     job is the tier ordinal, so the pip userLink prepends to the name is the same claim twice,
     four inches apart, at two sizes — the medallion and a 0.42em circle in front of the
     username. (It never showed on the owner's own plaque: an apex wears the crown INSTEAD of a
     pip, so the duplicate only ever hit everybody else.) Desktop is unaffected — the medallion
     is display:none there and the pip is the only thing carrying rank. */
  .prof-name .rank-pip{display:none}
  /* ⚠ AND A USERNAME MUST NOT RUN OFF THE PLAQUE. Mono + underscores gives no break
     opportunities, so `gravelpit_industries` overflowed the column at 390 and clipped at 320.
     The column is already minmax(0,1fr) — that lets the TRACK shrink; it does not let a word
     break. Both are needed. */
  /* ⚖ THE NAME TAKES THE WHOLE LINE, AND THE BADGES SIT UNDER IT (owner 2026-08-09: *"apex
     title should be next to 'you' under the username"*). Left to wrap naturally the row broke
     wherever it ran out of room — the apex pill stayed up beside the name and pushed `you` down
     alone, so the two badges that answer the same question ("what is this account to me") were
     on different lines. `flex:1 0 100%` makes that break deliberate instead of incidental.
     ⚠ ORDER, NOT DOM ORDER: the apex pill is appended right after the name (it reads as part of
     the title on desktop, where the row is one line and that is correct). `order:1` sorts it
     after `you` here without moving it in the tree, so desktop keeps 👑 name · reigning #1 and
     the phone gets you · reigning #1. Same for the role chip on everyone else's page. */
  .prof-name{overflow-wrap:anywhere;flex:1 0 100%}
  .prof-namerow .apex-pill{order:1}
  .prof-namerow > *{max-width:100%}
  /* ⚖ RHYTHM, AND IT IS THE OTHER HALF OF THE REPORT. Stacked, every block inherited whatever
     margin it wore as a column item, and `.prof-motto-wrap` + `.prof-motto` both carry 9px, so
     the motto sat 18px below the meta while the links under it sat 8px away — the gaps said the
     motto belonged with the meta when it belongs with the links that edit it. Scoped here
     rather than fixed globally: desktop's spacing was approved 2026-08-08 and reads correctly
     as a flex row. Three groups, tight inside and open between. */
  .plaque-meta{margin-top:10px}
  .prof-motto-wrap{margin-top:12px}
  .prof-motto-wrap .prof-motto{margin-top:0}
  .plaque-marks{margin-top:14px}
  /* ⚖ THE RECORD IS RULED OFF FROM THE IDENTITY. On desktop the score is a right-hand column
     and the whitespace does this job; stacked, it needs a line or the big number reads as one
     more thing in the list. */
  /* ⚑ PROTOTYPE forage-pia1.1 — THE RECORD IS ONE LINE, AND THE NUMBER GETS BIGGER, NOT SMALLER.
     Stacked it was three: the word SCORE, then 4200, then TRUST · WEIGHT — 84px for eleven
     characters, on a screen that already prints 4200 in the masthead and prints it AGAIN 30px
     lower in the score panel's own readout. As a baseline row the label sits beside the number
     and the measures ride the same baseline after it: 84px becomes 48, and the figure goes UP
     from 32px to 36 because it is the thing worth landing on. ⚖ NOTHING THE 2026-08-09 RULING
     SETTLED IS TOUCHED — the record still wears its hairline rule, still starts at the plaque's
     one left edge, and the strip still left-aligns (plaque.test.js pins all three). What changes
     is that three lines become one. ⚠ AND THE NAME ROW WAS LEFT ALONE ON PURPOSE: putting `evan`
     and its `you` chip on one line looks like 25px and buys ZERO, because the 84px avatar spans
     both identity rows and is the taller column — measured, not assumed. */
  .plaque-score{margin-left:0;text-align:left;
    border-top:1px solid var(--hair);margin-top:12px;padding-top:11px;
    display:flex;align-items:baseline;flex-wrap:wrap;gap:0 10px}
  .plaque-scoreval{font-size:36px}
  .plaque-stats{justify-content:flex-start;gap:4px 12px;margin-top:0}
  /* ⚠ THE LINKS STAY WITH THE MOTTO, and this is a decision rather than an oversight. Moving
     maintenance to the bottom of the plaque groups better on paper — identity, record, then
     chrome — but `mottoEditor` puts the "edit motto" TRIGGER in this row while the FORM it
     opens replaces `.prof-motto-wrap` above. Ordered apart on a phone, tapping the link scrolls
     nothing and changes something off-screen. So they sit 7px under the thing they edit. */
  .plaque-selfacts{margin-top:7px;gap:12px}
  /* ⚑ PROTOTYPE forage-pia1.1 — 18px of nothing under the record, above a hairline that already
     separates the plaque from the tabs. Six pixels do the same job on a phone. */
  .prof-plaque{padding-bottom:12px}
  /* ⚑ PROTOTYPE forage-pia1.1 — THE TAB ROW KEEPS EVERY TAB AND LOSES ITS PADDING. 16 above,
     14 below and 12 inside is 42px of margin around 62px of buttons; the buttons are untouched
     (same size, same tap floor, same two rows at 390) and the band closes to 24. */
  .prof-tabs{margin:10px 0 8px;padding-bottom:6px;gap:4px}
}

/* ==================== SOCIAL v1 (docs/SPEC_social.md) ==================== */
/* tag-add: input + submit arrow in a row; the ac dropdown spans below. */
.tagadd{display:flex;gap:6px;align-items:stretch}
/* ⚖ GAP FIRST, AND THAT IS THE WHOLE FIX ON THE SHARE LANDING (forage-fxj, 2026-08-21). MEASURED
   in the real app at 390x844 with touch, on the row the page's own `quickTagRow()` builds:
   input 264x44, arrow 46x44, **6.0px apart** — both controls were ALREADY at the floor and only
   the space between them was not, so `min-height` had nothing to do here and a blind one would
   have been pure damage. 10px, matching `.report-cats` in app-moderation.css, which took the
   same 6->10 today; the input is `flex:1 1 auto`, so it gives up the 4px and nothing else moves.
   ⚠ WHY IT IS SCOPED TO `.sh-quicktag` AND NOT PUT ON `.tagadd` ITSELF: this class is worn by the
   post page's add row, the profile's, and every queue row on /upload. Widening the base would
   re-space four screens to fix one, and the other three have not been measured.
   ⚠ AND WHY IT IS HERE RATHER THAN BESIDE `.sh-quicktag`'s other rules in app-rank.css: that file
   was at 1,000 of 1,000 lines (repealed 2026-08-29). `gap` is declared one line above this, which is where an override
   of it belongs anyway; app-profile.css loads after app-rank.css on all 21 pages.
   ⚠ COARSE ONLY, per app.css "THE 44px FLOOR, APPLIED" — *the floor exists for thumbs*. The
   1440 cell is `pointer:fine` and will keep reporting 6.0px; that is the probe applying a thumb
   rule to a mouse, and this repo has already answered it that way once today. */
@media(pointer:coarse){ .sh-quicktag .tagadd{gap:10px} }
.tagadd input{flex:1 1 auto;min-width:0}
.tagadd-go{flex:0 0 auto;border:1px solid var(--hair);background:var(--sunk);
  color:var(--accent);cursor:pointer;padding:0 12px;font-size:15px;line-height:1}
.tagadd-go:hover{border-color:var(--accent);color:var(--bright)}
.tagadd-go:disabled{opacity:.5;cursor:default}

/* post vote widget under the media (▲ net ▼). Reuses the terminal palette. */
.votewidget-wrap{display:flex;flex-direction:column;align-items:center;gap:4px;margin:12px 0 4px}
.votewidget{display:inline-flex;align-items:center;gap:10px;border:1px solid var(--hair);
  background:var(--panel);padding:6px 14px}
.pv{border:1px solid var(--hair);background:none;cursor:pointer;color:var(--faint);
  font-size:14px;line-height:1;padding:3px 9px}
.pv:disabled{opacity:.5;cursor:default}
.pv.up:hover:not(:disabled),.pv.up.on{color:var(--good);border-color:var(--good)}
.pv.down:hover:not(:disabled),.pv.down.on{color:var(--bad);border-color:var(--bad)}
.pv-net{font-variant-numeric:tabular-nums;min-width:2.2em;text-align:center;color:var(--ink);font-size:14px}
.pv-msg{min-height:1em}

/* comments — full-width section below the post grid. */
.comments{border:1px solid var(--hair);background:var(--panel);padding:14px 16px;margin:0 0 24px}
.comments h3{font-size:11px;letter-spacing:calc(.16em * var(--track));text-transform:var(--caps);color:var(--dim);margin-bottom:10px}
.comments .cc-count{color:var(--faint)}
/* position:relative — the @mention .ac dropdown anchors to the form (2026-08-02). */
.cform{display:flex;gap:6px;align-items:flex-start;position:relative}
/* the @mention dropdown above a bottom-pinned composer (the viewer sheet). */
.ac.up{top:auto;bottom:100%}
.cinput{flex:1 1 auto;min-width:0;resize:vertical;font-size:13px}
.cbtn.send{flex:0 0 auto;border:1px solid var(--hair);background:var(--sunk);color:var(--accent);
  cursor:pointer;padding:0 13px;font-size:15px;align-self:stretch}
.cbtn.send:hover{border-color:var(--accent);color:var(--bright)}
.cbtn.send:disabled{opacity:.5;cursor:default}
.cbtn.embed{flex:0 0 auto;border:1px solid var(--hair);background:var(--sunk);color:var(--dim);
  cursor:pointer;padding:0 11px;font-size:13px;align-self:stretch}
.cbtn.embed:hover{border-color:var(--accent);color:var(--accent)}
/* COMMENT POST-EMBED v1 (owner ruling 2026-07-21x): the composer's pending-embed
   chip (removable, before posting) and the rendered card (after posting).
   ── SIZE (owner report 2026-08-03: *"the post embedding … needs to be fully size on
   mobile"*). Both were squares — a 34px chip thumb and a 220x180 cover-cropped card — so
   neither told you what the post actually WAS. Both now render the whole frame at its own
   aspect (the width follows from the inline aspect-ratio common.js setEmbedMedia stamps on,
   so nothing letterboxes and nothing reflows on decode) with the HEIGHT capped.
   --emb-prev-h is that cap, declared once and shared with the viewer sheet's chip and card
   below, so the four embed surfaces cannot drift apart in pixels the way they drifted in
   shape. It is a min() of a px ceiling and a vh share: the px keeps a tablet from handing a
   comment box half the screen, the vh keeps a small phone's comment list from being eaten by
   chrome — that budget is pinned in tools/viewer_e2e.sh and is the reason this is a cap at
   all rather than "as big as it wants". */
:root{--emb-prev-h:min(150px,18vh);--emb-card-h:min(220px,26vh)}
.cembed-chip{display:flex;flex-direction:column;align-items:stretch;gap:6px;margin-top:6px;padding:5px 8px 8px;
  border:1px solid var(--hair-hi);border-radius:6px;background:var(--sunk);font-size:11px;color:var(--dim)}
/* the label row sits ABOVE the media so the ✕ never lands on the picture */
.cembed-row{display:flex;align-items:center;gap:8px;min-height:22px}
.cembed-thumb{align-self:center;height:var(--emb-prev-h);width:auto;max-width:100%;
  object-fit:contain;border-radius:3px;flex:0 0 auto;background:var(--void)}
.cembed-label{flex:1 1 auto;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.cembed-remove{position:relative;flex:0 0 auto;background:none;border:0;color:var(--faint);cursor:pointer;font-size:12px;padding:2px 4px}
.cembed-remove:hover{color:var(--bad)}
/* ⚖ THE FRAME IS GONE HERE TOO (owner ruling 2026-08-03, *"kill the framing"*). The complaint
   was made against the viewer sheet, but this is the SAME SHAPE — a bordered --sunk plate
   around a posted embed — rendered by post.html's embedCard(). Fixing the sheet and leaving
   the post page boxed is precisely the drift HANDOVER records for renderComment and
   appliedVoteControls, so both call sites moved together. The ROUNDING stays and grows: it is
   the one edge the reference actually has. */
.cembed-card{display:inline-flex;position:relative;margin-top:8px;max-width:100%;
  border-radius:10px;overflow:hidden}
.cembed-card img{display:block;height:var(--emb-card-h);width:auto;max-width:100%;object-fit:contain}
.cembed-badge{position:absolute;top:5px;right:5px;font-size:9px;letter-spacing:calc(.08em * var(--track));text-transform:var(--caps);
  padding:2px 5px;border-radius:3px;background:rgba(9,10,12,.74);color:var(--accent)}
.cembed-title{position:absolute;left:0;right:0;bottom:0;padding:5px 7px;font-size:10.5px;color:#fff;
  background:linear-gradient(transparent,rgba(0,0,0,.78));white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.cformbox{margin:0 0 12px}
.cmsg{min-height:0}
.commentlist{display:flex;flex-direction:column;gap:12px}
.comment{display:flex;gap:10px;align-items:flex-start}
.comment.reply{margin-left:6px}
.comment.removed .ctext{color:var(--faint);font-style:italic}
.cvote{display:flex;flex-direction:column;align-items:center;gap:2px;flex:0 0 auto}
.cvote .pv{position:relative;padding:1px 5px;font-size:11px}
.cvote .pv-net{min-width:1.6em;font-size:12px}
.cmain{flex:1 1 auto;min-width:0}
.cmeta{font-size:11px;color:var(--dim);margin-bottom:3px;display:flex;align-items:center;gap:2px}
.cmeta .cauthor.dim{color:var(--faint)}
.cmeta .cyou{color:var(--faint)}
.ctext{font-size:13px;color:var(--ink);white-space:pre-wrap;word-break:break-word;line-height:1.45}
.cactions{display:flex;gap:12px;margin-top:5px}
.clink{position:relative;background:none;border:none;color:var(--faint);cursor:pointer;font-size:10.5px;
  letter-spacing:calc(.08em * var(--track));text-transform:lowercase;padding:0}
.clink:hover{color:var(--accent)}
.clink.danger:hover{color:var(--bad)}
.clink:disabled{opacity:.6;cursor:default}

/* ══ THE COMMENT THREAD'S 44px, ONE PASS, GAPS FIRST (forage-fxj.1, 2026-08-21) ═══════════════
   ⚑ EVERY NUMBER BELOW WAS MEASURED, on /post at 390x844 WITH TOUCH, on a real four-deep thread
   in the real app — because the bead this closes exists to stop a blind min-height, and the
   arithmetic it argued from turned out not to be this screen's arithmetic.

   ⚠ THE BEAD SAID `.clink` IS "FOUR TO FIVE LINKS IN ONE ROW 5px APART", so 44px boxes would
   "OVERLAP BY 11px" and aiming at `reply` would land on `delete`. MEASURED, `.cactions` is
   `gap:12px` (three lines up) and the links are **39x32 / 47x32 / 43x32, 12px apart** — the 5px
   belongs to a different row on a different surface. The overlap that made this a P2 is not
   there. What IS there is the same answer anyway, for a better reason:

   ⚖ VERTICAL IS FREE AND HORIZONTAL IS GONE. A comment on this thread is 86-160px tall, so 6px
   of vertical overhang each way reaches nothing. Sideways there is nothing left at ANY depth:
   `.creplies` indents 12px per level and at depth 3 the text column is ~150px wide, so the
   `reply delete edit` row already ends 24px from the screen edge. A horizontal halo would spend
   the 12px gap and put `delete` under a thumb aiming at `reply` — the exact mis-tap the bead
   predicted, arriving from the repair rather than from the layout. Same conclusion as
   docs/DECISIONS.md "The floor may not change what a member can see" reached for `.btn`.
   ⚠ `reply` stays **39px WIDE** and that is deliberate, not an oversight: the floor is spent on
   the axis a thumb misses on in a scrolling list, and there is no width to spend. The `.btn`
   ruling says the same in as many words — a narrow control stays narrow rather than stealing
   its neighbour's taps.

   ⚖ AND THE VOTE ARROWS ARE A PARTIAL, WITH THE ARITHMETIC. `.cvote .pv` measures **19x15**,
   stacked, `gap:2px`, with the net between them — the whole column is **53px tall**. Two 44px
   boxes stacked need 88px plus a gap, against a comment body measured at 82px, so a real floor
   here would make EVERY comment ~96px tall: precisely the "blind min-height makes it worse" this
   bead was carved out for. ⛔ AND SIDEWAYS IS WORSE: the arrows measure **0.3px** from `.crail`,
   which is already a PARTIAL waiver for the mirror-image reason (13px of target that collapses
   the thread), so one pixel of horizontal halo makes `upvote` collapse a thread instead.
   So the arrows take the ONE gap that exists — `.pv-net` between them is a `<span>` with no
   handler, 16px of dead screen — and TILE across it: 15px -> 34px, 2.3x the area, stealing
   nothing. Still under the floor, waived at the measured number rather than hidden.
   ⚖ THE REAL FIX IS A LAYOUT DECISION AND IT IS THE OWNER'S: /s already answers this by laying
   the pair out HORIZONTALLY under the comment, where the same `.pv` measures **44x44 with 8px
   gaps** (MEASURED on /s/bugs at 390x844). Bringing that shape to /post's thread changes what
   the thread looks like, which is not a tap-target repair. Filed, not guessed. */
/* ⚠ `.clink`'s OVERHANG GROWS **UPWARD ONLY**, AND THAT WAS FOUND BY MEASURING THE FIX, not by
   reading it. A centred 44px box on a 32px row reaches 6px BELOW the action row — and the next
   thing down is the CHILD comment's upvote arrow, which measured x 87-106 against `reply`'s
   x 68-107. The two halos overlapped by 5.5px directly under the word `reply`, so a low tap on
   `reply` would have upvoted the reply beneath it: a mis-tap introduced BY the repair, which is
   the whole thing this bead was carved out to prevent. Bottom-anchored, the 12px goes up into
   `.ctext` — prose, and this comment's own — and the overlap is 0. */
@media(pointer:coarse){
  .clink::after,.cembed-remove::after{content:"";position:absolute;left:0;right:0;bottom:0;
    height:var(--tap-min)}
  .cvote .pv::after{content:"";position:absolute;left:0;right:0;top:50%;
    transform:translateY(-50%);height:34px}
}
.creplies{margin-top:10px;padding-left:12px;border-left:1px solid var(--hair);
  display:flex;flex-direction:column;gap:10px}
.cempty{color:var(--faint)}
@media(max-width:820px){.comments{padding:12px}}


/* ════ THE TRUST TAB (owner ask 2026-08-13; profTrustTab in common-points.js) ═════════════════
   Explains a number that was previously just a percentage: where it came from, what the next
   rung costs, and what each rung buys. The rungs are drawn ON the bar because a threshold you
   cannot see is a threshold nobody aims at.
   ⚠ THE PREFIX IS `ptrust-`, NOT `tr-`, AND THAT IS NOT STYLE. app-moderation.css:433 already
   owns `.tr-head` for the tag-review strip — a different feature entirely — and it declares
   `align-items:center`. Loading earlier, it centred this tab's column and collapsed the trust
   bar to zero width, because a bar is an empty div whose only children are absolutely
   positioned. Nothing in either file was wrong; the names were. Only a render caught it. */
.prof-trust { display: flex; flex-direction: column; gap: 14px; padding: 4px 0; }
.ptrust-head { display: flex; flex-direction: column; gap: 8px; }
.ptrust-now { font-size: 30px; line-height: 1; color: var(--ink); font-variant-numeric: tabular-nums; }
.ptrust-bar { position: relative; height: 12px; border-radius: 6px; background: var(--sunk);
  box-shadow: inset 0 0 0 1px var(--hair); overflow: visible; }
.ptrust-bar > i { display: block; height: 100%; border-radius: 6px;
  background: linear-gradient(90deg, var(--accent), var(--good)); }
/* a rung: a hairline through the bar, brighter once you are past it */
.ptrust-rung { position: absolute; top: -3px; bottom: -3px; width: 2px; background: var(--hair-hi);
  transform: translateX(-1px); }
.ptrust-rung.past { background: var(--good); }
.ptrust-next { font-size: 12.5px; color: var(--ink); }
.ptrust-rows, .ptrust-earn { display: flex; flex-direction: column; gap: 2px; }
.ptrust-earnhead { text-transform: var(--caps); letter-spacing: calc(.12em * var(--track)); font-size: 9.5px; }
.ptrust-row { display: grid; grid-template-columns: 1fr auto auto; gap: 10px; align-items: baseline;
  padding: 5px 0; border-bottom: 1px solid rgba(255,255,255,.04); font-size: 12px; }
.ptrust-row:last-child { border-bottom: 0; }
.ptrust-what { color: var(--dim); min-width: 0; }
.ptrust-n { color: var(--faint); font-size: 11px; }
.ptrust-d { color: var(--ink); min-width: 4.5rem; text-align: right; }
.ptrust-row.good .ptrust-d { color: var(--good); }
.ptrust-row.bad .ptrust-d { color: var(--bad); }
.ptrust-sum { font-variant-numeric: tabular-nums; }
