/* app-tagrows.css — UNIFORM TAG ROWS — the applied-tag list, everywhere it appears.

   ⚖ 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 3000-3558). 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. */
/* ============================================================
   UNIFORM TAG ROWS (owner ruling 2026-07-19f) — the applied-tag list on the queue,
   collective, and post surfaces renders as full-width ROWS (name left, truncating
   with ellipsis; the ▲▼ vote column fixed on the right edge, identical size +
   position on every row for thumb muscle memory) instead of reflowing chips.
   Voting can't move the buttons — the name area (flex:1) absorbs the net-score
   width change. Variant idioms carry onto the row as a LEFT ACCENT (canonical =
   solid, community = ◆/info, ai = dashed, nsfw·nsfl = colored). Overrides the chip
   layout (.qtags .tag, .ctl .tag) — placed last to win on equal specificity.
   ============================================================ */
/* owner report 2026-07-22n: the row list previously capped itself to 560px on
   desktop ("don't stretch; votes stay right-anchored") — narrower than the
   "add tag" box and every other element around it on both post.html AND
   queue.html/collective.html, at ANY desktop width, not just the 720-820px
   post.html case an earlier fix here scoped it away from. No cap anymore —
   the row always fills its real container width, full stop. */
/* ⚠ THE ROWS ARE BOXES NOW (owner report 2026-07-31n: "these dont have boxes either").
   This block used to say `border:0; border-bottom:1px; border-left:2px transparent` — a
   LIST-ROW idiom that predates the taxonomy, and being (0,2,0) declared later than the
   provenance rules it beat them outright: the post page's tags had no box at all, so
   dashed/solid/double could not show. It now declares NO border, and inherits `.tag`'s
   1px + whatever the provenance state sets. */
.taglist.tagrows{display:flex;flex-direction:column;gap:6px;width:100%}
.tagrows .tag{display:flex;align-items:center;width:100%;min-height:44px;gap:8px;
  padding:0 0 0 10px;background:none;font-size:12px;transition:background .12s}
.tagrows .tag .tagrow-nm{flex:1 1 auto;min-width:0;display:flex;align-items:baseline;gap:6px;overflow:hidden}
.tagrows .tag .tagrow-nm .nm{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:inherit}
/* same ruling as .searchtags .tnet — the tag name outranks its score; see the note there. */
.tagrows .tag .tnet{flex:0 500 auto;min-width:0;overflow:hidden;white-space:nowrap;
  color:var(--faint);font-variant-numeric:tabular-nums;letter-spacing:calc(.02em * var(--track))}
.tagrows .tag .aidot{flex:0 0 auto}
/* the right control cluster — votes are ALWAYS the rightmost, fixed column */
.tagrows .tag .tagrow-ctl{flex:0 0 auto;display:flex;align-items:center;gap:0;margin-left:auto}
.tagrows .tag .applied-votes,.tagrows .tag .tv{margin:0;display:flex;gap:0;border:0;padding:0}
.tagrows .tag .tv button,.tagrows .tag .applied-votes button{
  min-width:44px;min-height:44px;display:flex;align-items:center;justify-content:center;
  background:none;border:0;border-left:1px solid var(--hair);color:var(--faint);
  cursor:pointer;font-size:14px;line-height:1;padding:0}
.tagrows .tag .tv button:hover,.tagrows .tag .applied-votes button:hover{color:var(--ink);background:var(--raised)}
.tagrows .tag .tv button.up.on,.tagrows .tag .tv button.yes.on,
.tagrows .tag .applied-votes button.up.on,.tagrows .tag .applied-votes button.yes.on{color:var(--good)}
.tagrows .tag .tv button.down.on,.tagrows .tag .tv button.no.on,
.tagrows .tag .applied-votes button.down.on,.tagrows .tag .applied-votes button.no.on{color:var(--bad)}
/* post.html extra controls (confirm ✓ / remove ✕ / admin verdicts) — left of the votes */
.tagrows .tag .tagrow-ctl .ok,.tagrows .tag .tagrow-ctl .x{
  min-width:34px;min-height:44px;display:flex;align-items:center;justify-content:center;font-size:13px}
.tagrows .tag .verdicts{display:flex;gap:0}
.tagrows .tag .verdicts button{min-width:38px;min-height:44px;background:none;border:0;border-left:1px solid var(--hair);cursor:pointer}
/* ⚠ THE LEFT-STRIPE ACCENTS ARE GONE — the whole box carries the state now, and a stripe
   saying the same thing in a different colour was the reason `ai` rows rendered grey instead
   of their kind colour. The `.nm` COLOUR overrides went with them: at (0,4,0) they beat every
   kind rule, which is why the post page ignored kind colours entirely. The dotted underline
   and the ⟡ dot stay — they are the redundant cue for the dashed border, which matters at
   10px in sunlight. ⚠ The COMMUNITY ◆ did NOT survive that argument: owner ruling
   2026-08-05, "the blue diamond is redundant". Its border is the whole cue now. */
.tagrows .tag.ai .aidot{color:var(--faint)}
/* the warnings KEEP their name colour here: they agree with the global warning rule and a
   content warning must never be the thing that loses a cascade fight. */
.tagrows .tag.nsfw .nm{color:var(--cat-nsfw)}
.tagrows .tag.nsfl .nm{color:var(--cat-nsfl)}
.tagrows .tag.tag-removing{opacity:0;transform:translateX(8px)}

/* collapsed-by-default tag section on queue + collective: a summary line that is the
   whole tap target ("tags · N ▸"), expanding to the row list + the add box. */
.qtags-summary{position:relative;overflow:hidden;display:flex;align-items:center;justify-content:space-between;width:100%;
  min-height:40px;background:none;border:0;border-bottom:1px solid var(--hair);
  color:var(--dim);font-size:11px;letter-spacing:calc(.14em * var(--track));text-transform:var(--caps);
  cursor:pointer;padding:0 2px;text-align:left;-webkit-user-select:none;user-select:none;touch-action:none}
.qtags-summary:hover{color:var(--ink)}
.qtags-caret{color:var(--faint);font-size:10px}
.qtags-body{padding-top:2px}
/* HOLD-TO-PEEK fill (owner ruling 2026-07-19h): a progress bar that grows across the
   summary over the hold; releasing early snaps it back. No layout impact (absolute).
   Reduced-motion: JS skips the fill animation (a delayed reveal instead). */
.qtags-holdfill{position:absolute;left:0;bottom:0;height:2px;width:0;background:var(--accent);pointer-events:none}

/* BLIND TAGGING (owner ruling 2026-07-19g): the add bar renders first; a blind hint
   sits under it (dropped once the box is expanded / blind is burned); a convergence
   ×2 note is accent-gold. */
.qaddwrap{margin:10px 0 4px}
.qblindhint{font-size:10.5px;letter-spacing:calc(.03em * var(--track));color:var(--accent);margin-top:5px;opacity:.85}
.tzmsg.x2{color:#ffd98a}

/* SEARCH-CONTEXT TAG CURATION BAR (owner ruling 2026-07-19n) — under a grid card, one
   uniform row per SEARCHED tag: name + dim "ai" source hint left, actions right-anchored
   (member ▲▼, admin ✓✕). Scaled to card width; the vote column keeps a thumb-sized 34px
   target (tighter than the 44px full-row bar — a grid card is small, but still tappable). */
.searchtags{border:1px solid var(--hair);border-top:0;background:var(--sunk);
  display:flex;flex-direction:column}
/* sort context line under a tile (owner ruling 2026-07-20c): the value the active sort orders by. */
.sortctx{padding:3px 7px;font-size:10px;letter-spacing:calc(.08em * var(--track));color:var(--faint);
  text-transform:var(--caps);line-height:1.4}
.sortctx.num{font-variant-numeric:tabular-nums;color:var(--dim)}
/* ⚠ THE BOXES ARE GONE HERE — SUPERSEDES the 2026-07-31d ruling (owner: "these need colored
   boxes and the tags on the posts really need the boxes to show if they are reenforced or not
   now"). That ruling was RIGHT FOR ITS TIME and the reason it existed has since been removed:
   provenance had nothing but the border to paint, so a row without a box showed no dashed, no
   solid and no double. Since 2026-08-04b the TYPE carries it — italic = ai, bold+underline =
   reinforced — and the box became four borders' worth of horizontal space bought for a signal
   that no longer needs them. On a grid tile that cost was real and visible: `caption_scramble`
   rendered as `caption_s…` (owner report 2026-08-04d) because the borders ate the width the
   name needed. A tag you cannot read is not worth a box that says what the letters already say.
   ⚠ THE BAR IS THE KIND, AND THAT IS DELIBERATE (same owner report: "make the bar itself
   slightly colored based on the tag type"). It is drawn from `currentColor`, which the
   `.k-<kind>.k-<kind>` rules already set — so it needs NO per-kind entry and a kind added later
   is coloured for free. Same reason the saturate() approach was chosen over colour tokens.
   The two-channel law survives the loss of the box intact, just relocated: the bar's COLOUR is
   the kind, the bar's STYLE is the provenance (dashed = ai), exactly as the border read.
   ⚠ (0,3,0) IS LOAD-BEARING — `.strow.tag`, not `.strow`. "THE LAST WORD ON TAG COLOUR" lives
   at the END of this file and `.tag.tag-reinforced` is (0,2,0); a plain `.searchtags .strow`
   ties it and LOSES on order, so the inset ring and the border would come straight back. This
   is the same specificity trap that cost the post page its kind colours once already. */
.searchtags{gap:5px;padding:5px}
/* position:relative is LOAD-BEARING — the sv-up/sv-dn vote wedge is an absolutely positioned
   ::before on this element. Without it the wedge resolves against some ancestor and lands
   somewhere else on the page entirely. Pinned in tagtaxonomy.test.js. */
/* ⚠ NO TEXT SELECTION ON A ROW YOU HOLD (owner report 2026-08-05b). A long press on selectable
   text raises the OS selection handles and the magnifier, which swallow the gesture mid-hold —
   the vote never arms and the user gets a text cursor instead. The prototype had this and the
   port dropped it, which is the whole bug. -webkit-touch-callout kills the iOS "copy/share"
   sheet, which is a second, separate interceptor. */
.searchtags .strow{position:relative;display:flex;align-items:center;gap:6px;min-height:34px;
  padding:0 0 0 7px;font-size:11px;
  user-select:none;-webkit-user-select:none;-webkit-touch-callout:none}
.searchtags .strow.tag{border:0;border-radius:0;box-shadow:none;
  border-left:2px solid currentColor;
  background:color-mix(in srgb, currentColor 7%, transparent)}
/* provenance keeps its line STYLE on the bar; the type marks carry the rest. */
.searchtags .strow.tag.tag-suggested{border-left-style:dashed}

/* ⚠ THE UNDERLINE DECORATES THE NAME, NOT THE ROW (owner report 2026-08-04j: "have an
   underline on the score"). text-decoration PROPAGATES to in-flow descendants and a child
   cannot cancel it — only an atomic inline escapes, and a flex item is blockified, so `.tnet`
   could never opt out. The line ran under "· +5.69" as if the score were part of the tag.
   So the surfaces whose chip holds MORE than a name move the decoration down onto `.nm`.
   Chips that are nothing BUT the name (the viewer's .vw-tag) keep it on the element and are
   unaffected. (0,3,0) beats the (0,2,0) state rule at the end of this file regardless of order. */
.searchtags .strow.tag-reinforced,.tagrows .tag.tag-reinforced,.ctl .tag.tag-reinforced{
  text-decoration:none}
.searchtags .strow.tag-reinforced .nm,.tagrows .tag.tag-reinforced .nm,
.ctl .tag.tag-reinforced .nm{
  text-decoration:underline;text-decoration-thickness:1px;text-underline-offset:2px}
/* ⚠ YOUR OWN VOTE IS A CORNER WEDGE (owner ruling 2026-08-04i). Since reveal-on-tap the ▲▼ are
   off screen at rest, so their `.on` state — the only record that you had voted — was invisible
   and a tag you upvoted looked exactly like one you never touched.
   ⚠ WHY NOT TINT THE BAR, WHICH WAS THE FIRST IDEA AND IS THE OBVIOUS ONE: the vote colours
   COLLIDE WITH THE KIND COLOURS THEY WOULD SIT ON. --good #5fc46b against --cat-humor #6fe0b0
   is green on green, and --cat-nsfl IS `var(--bad)` — byte-identical, so a downvote mark on a
   gore tag would be invisible, on the one kind that most needs to stay legible. A wedge sits on
   the row's near-black GROUND instead of on the kind colour, so no pairing can cancel.
   ⚠ POSITION CARRIES THE MEANING, not just the colour: top-left = up, bottom-left = down, which
   is the same up/down axis the vote gesture uses. Someone who cannot separate the two hues
   still reads it, and it survives being the only cue on a colour-blind display.
   ⚠ COSTS NO WIDTH. It lives in the dead space beside the kind bar — deliberate, after two
   rounds spent reclaiming horizontal space from `caption_s…`. Never give this a layout box.
   ⚠ ::before IS TAKEN BY THIS. A future :before on .strow silently deletes the vote mark. */
.searchtags .strow.tag.sv-up::before,.searchtags .strow.tag.sv-dn::before{
  content:"";position:absolute;left:3px;border-left:7px solid currentColor;pointer-events:none}
.searchtags .strow.tag.sv-up::before{top:0;border-bottom:7px solid transparent;color:var(--good)}
.searchtags .strow.tag.sv-dn::before{bottom:0;border-top:7px solid transparent;color:var(--bad)}
/* ⚠ WRAP, NEVER CLIP (owner report 2026-08-04e: "it's still trunked"). Removing the boxes
   bought width and it still was not enough — a half-width grid tile leaves ~110px for the name
   after 7px padding, a 2px bar and TWO 34px vote targets, and `caption_scramble` needs more
   than that at 11px. The buttons cannot give: 34px is already below the touch-target minimum,
   and shrinking a tap target on the mobile-first surface to fit text is the wrong trade.
   So the name gets the extra LINE instead of an ellipsis. This is the same doctrine already
   settled twice on this exact row — "the tag outranks its score" (2026-08-01), and
   keepWholeOrHide() dropping a number entirely rather than render a clipped "+5.6" when the
   truth is "+5.64". A half-read tag is a wrong tag: `caption_s…` could be caption_scramble or
   caption_swap, and the reader cannot tell.
   `overflow-wrap:anywhere` rather than `break-word` because a tag is ONE token — there is no
   space to break at, so break-word would decline and clip anyway.
   The row grows by one line only when it has to; min-height:34px still governs the common case,
   and a two-line 11px name (~31px) mostly fits inside it without moving anything at all. */
.searchtags .strow{padding-top:3px;padding-bottom:3px}
.searchtags .strow .strow-nm{flex:1 1 auto;min-width:0;display:flex;align-items:baseline;gap:5px;overflow:visible}
.searchtags .strow .strow-nm .nm{min-width:0;overflow:visible;text-overflow:clip;
  white-space:normal;overflow-wrap:anywhere;color:inherit}
.searchtags .strow .st-src{flex:0 0 auto;font-size:8.5px;letter-spacing:calc(.14em * var(--track));text-transform:var(--caps);color:var(--faint)}
/* ⚖ THE TAG OUTRANKS ITS SCORE (owner ruling 2026-08-01). This was `flex:0 0 auto`, i.e. the
   number could never shrink, so on a narrow grid tile the NAME was the only thing left to
   give and "offensive · +5.64" rendered as "off… · +5.64" — the score kept in full while the
   thing it describes became unreadable. A tag you cannot read is not worth a score you can.
   The number now yields first (a very large shrink factor takes essentially all of the
   overflow before the name loses a pixel), and `keepWholeOrHide()` in common.js removes it
   outright the moment it cannot render IN FULL — a clipped "+5.6" where the truth is "+5.64"
   is worse than no number at all. */
.searchtags .strow .tnet{flex:0 500 auto;min-width:0;overflow:hidden;white-space:nowrap;
  color:var(--faint);font-variant-numeric:tabular-nums}
.searchtags .strow .st-msg{flex:0 0 auto;margin-right:6px;font-size:10px}
/* ⚠ THE VOTE CONTROLS ARE HIDDEN UNTIL ASKED FOR (owner ruling 2026-08-04f). Two 34px targets
   were eating ~68px of a ~195px half-tile row, which is why the name truncated even after the
   boxes came off; wrapping fixed the symptom, this removes the cause. The name now gets the
   whole row by default and cannot truncate at all.
   ⚠ WHY NOT SHRINK THEM: 34px is already under the touch-target minimum. Trading tap accuracy
   for text width on the mobile-first surface is the wrong direction.
   ⚠ WHY NOT A HOLD-AND-SLIDE (considered, 2026-08-04f): "hold + slide vertically" is ALREADY
   the Mode Dial's gesture (SPEC_tag_taxonomy §6, xp-search.js) — it picks the admin verb. A
   second one meaning "cast a vote" splits the vocabulary. And a misfire is not free here:
   SPEC_crowd_tags makes wrong voters lose score AND weight, so an accidental slide inside a
   vertically-scrolling grid costs the voter real reputation. The dial deliberately makes its
   null result the SAFE verb; slide-to-vote inverts that.
   Hover-capable pointers keep the old behaviour outright — there is no space pressure on a
   desktop row and no reason to make a mouse user click twice. */
/* ⚠ ON TOUCH THERE ARE NO BUTTONS AT ALL (owner ruling 2026-08-05). Reveal-on-tap was chosen
   before the hold-and-slide gesture existed to compare against; once it did, the gesture won and
   the revealed arrows were just "ugly buttons" occupying a row that had already lost its name to
   them once. A finger holds the tag and slides; see HOLD-VOTE below.
   Desktop keeps the arrows outright — hold-and-slide means nothing with a mouse, and a wide row
   has no space pressure to solve. One surface, two input models, decided in CSS. */
.searchtags .strow .strow-ctl{flex:0 0 auto;display:none;align-items:center;gap:0;margin-left:auto}
@media (hover:hover) and (pointer:fine){.searchtags .strow .strow-ctl{display:flex}}

/* ════ HOLD-VOTE — the dial, ported from the prototype ═══════════════════════════════════
   Hold a tag; a vertical bar opens CENTRED ON THE THUMB with both stops already visible;
   slide to one and release. Releasing at rest does nothing, so the null result is the safe one
   — the same shape and the same safety rule as the Mode Dial (SPEC_tag_taxonomy §6), which is
   why this reads as the archive's one hold-and-slide idiom rather than a second one.
   ⚠ EVERYTHING READABLE SITS CLEAR OF THE CONTACT PATCH. The first build put a knob under the
   thumb, i.e. under the one part of the screen the user cannot see. The BEAM grows from centre
   toward the stop instead, and the stops park outside a thumb's footprint. */
/* ⚠ THE DIAL CARRIES ITS OWN GROUND (owner report 2026-08-05b: visibility). The prototype was
   judged on a dark page; the real grid puts it over whatever meme is underneath — a yellow
   NOTICE sign, a white comic — and hairlines plus grey micro-labels simply vanished on those.
   A radial scrim travels with it: dense at the centre where the reading happens, gone by the
   edge so it never reads as a modal. */
.hv-dial{position:fixed;z-index:var(--z-hovercard);left:0;top:0;pointer-events:none;opacity:0;
  transform:translate(-50%,-50%);transition:opacity .1s;--hv-stop:86px}
.hv-dial::before{content:"";position:absolute;left:50%;top:50%;width:300px;height:calc(var(--hv-stop)*2 + 150px);
  transform:translate(-50%,-50%);border-radius:50%;
  background:radial-gradient(ellipse at center, color-mix(in srgb,var(--void) 92%,transparent) 0%,
    color-mix(in srgb,var(--void) 78%,transparent) 42%, transparent 72%)}
.hv-dial.on{opacity:1}
/* the labels sit on that scrim, so they get real ink rather than --faint grey */
.hv-stop i{color:var(--ink)}
.hv-line{opacity:.55}
.hv-line{position:absolute;left:50%;top:50%;width:1px;margin-left:-.5px;
  height:calc(var(--hv-stop)*2 + 44px);transform:translateY(-50%);background:var(--hair-hi)}
/* ⚠ TWO BEAMS, EACH PERMANENTLY ANCHORED — never one that re-anchors. A single beam had to swap
   `top` AND `transform-origin` to point upward, so releasing snapped it back to the downward
   anchor while its inline scaleY was still set and the bar visibly flashed DOWN through the
   fade-out. Anchors that never move cannot invert. */
.hv-beam{position:absolute;left:50%;width:3px;margin-left:-1.5px;height:var(--hv-stop);
  background:currentColor;transform:scaleY(0);transition:transform .07s linear;
  box-shadow:0 0 10px currentColor}
.hv-beam-up{top:calc(50% - var(--hv-stop));transform-origin:bottom center}
.hv-beam-dn{top:50%;transform-origin:top center}
/* a teardown must never be watchable */
.hv-dial:not(.on) .hv-beam,.hv-dial:not(.on) .hv-stop,.hv-dial:not(.on) .hv-rest{transition:none}
.hv-rest{position:absolute;left:50%;top:50%;transform:translate(-50%,-50%);width:46px;height:46px;
  border:1px solid var(--hair-hi);border-radius:50%;display:flex;align-items:center;
  justify-content:center;font-size:9px;color:var(--faint);
  background:color-mix(in srgb, var(--void) 82%, transparent);transition:all .1s}
/* ⚠ THE REST RING NEVER CHANGES SIZE (owner report 2026-08-05b: "effect changing"). It used to
   gain a 4px spread shadow when centred, so the element directly under the thumb visibly grew
   and shrank as you crossed the threshold — the one place on screen where movement reads as
   the UI glitching rather than responding. Only the COLOUR moves now; the geometry is fixed. */
.hv-dial.mid .hv-rest{border-color:var(--bright);color:var(--bright)}
/* ⚠ THE STOPS ARE MIRRORED, NOT COPIED. Both once read glyph-then-label, so on the UP side the
   label wedged between the beam tip and the arrow and the line visibly stopped short of the
   thing it was selecting. Up is anchored by its BOTTOM edge and reads label-then-glyph; down is
   anchored by its TOP. Both glyphs land ON the beam tip whatever the label says. */
.hv-stop{position:absolute;left:50%;transform:translateX(-50%);display:flex;flex-direction:column;
  align-items:center;gap:1px;transition:all .1s;opacity:.34}
.hv-up{bottom:calc(50% + var(--hv-stop) - 6px);transform-origin:center bottom}
.hv-dn{top:calc(50% + var(--hv-stop) - 6px);transform-origin:center top}
.hv-stop b{font-size:19px;line-height:1;font-weight:400;display:block}
.hv-stop i{font-style:normal;font-size:7.5px;letter-spacing:calc(.16em * var(--track));
  text-transform:var(--caps);white-space:nowrap;color:var(--faint)}
.hv-dial.up .hv-up{opacity:1;color:var(--good);transform:translateX(-50%) scale(1.3)}
.hv-dial.up .hv-up i{color:var(--good)}
.hv-dial.dn .hv-dn{opacity:1;color:var(--bad);transform:translateX(-50%) scale(1.3)}
.hv-dial.dn .hv-dn i{color:var(--bad)}
.hv-dial.up .hv-beam-up{background:var(--good);box-shadow:0 0 12px var(--good)}
.hv-dial.dn .hv-beam-dn{background:var(--bad);box-shadow:0 0 12px var(--bad)}
/* ⚠ THE STOPS WEAR THE LATCHED VERB (2026-08-09). The hold-and-slide is the ONLY curation
   input a phone has on the search grid, and until now it said "upvote / downvote" whatever the
   ModeDial was latched to — then cast a plain vote, which is the bug this pairs with. The
   words come from MODVERB (common-curation.js hvPaintVerbs); these three rules give them the
   archive's existing verb colours, and they are declared AFTER `.hv-dial.up .hv-up` (0,3,0)
   and `.hv-dial.up .hv-up i` (0,3,1) so an equal-specificity tie is broken by source order —
   the same trap `.vote.vote.mv-*` was written to survive (see modverb.test.js). */
.hv-dial .hv-stop.mv-reinforce,.hv-dial .hv-stop.mv-reinforce i{color:var(--good)}
.hv-dial .hv-stop.mv-remove,.hv-dial .hv-stop.mv-remove i{color:var(--bad)}
.hv-dial .hv-stop.mv-punish,.hv-dial .hv-stop.mv-punish i{color:var(--accent)}
/* the armed row says WHICH tag, so the dial needs no label of its own */
.searchtags .strow.hv-armed{background:color-mix(in srgb, currentColor 26%, transparent);
  outline:1px solid currentColor;outline-offset:-1px}
/* ⚠ ARMED = THE PAGE IS FROZEN. touch-action stops the browser STARTING a scroll;
   overscroll-behavior kills the rubber-band; common.js adds a non-passive touchmove blocker for
   anything already in flight. All three, because each alone leaks. NOT position:fixed/overflow
   on body — those reset scrollTop and the page jumps the instant it arms. */
body.hv-locked{touch-action:none;overscroll-behavior:none}
/* ⚠ THE PULSE OUTLIVES THE DIAL, on purpose: the dial is gone the instant you lift, so this is
   the only thing left that says which way it went. Its own element, so dismissing the dial
   cannot cut the confirmation short. */
.hv-pulse{position:fixed;left:0;top:0;pointer-events:none;z-index:var(--z-hovercard);opacity:0}
.hv-pulse span{position:absolute;left:0;top:0;border-radius:50%;transform:translate(-50%,-50%)}
.hv-ring{width:52px;height:52px;border:2px solid currentColor}
.hv-glow{width:52px;height:52px;background:radial-gradient(circle,currentColor 0%,transparent 68%)}
.hv-pulse.go{opacity:1}
.hv-pulse.go .hv-ring{animation:hvRing .46s cubic-bezier(.18,.72,.28,1) forwards}
.hv-pulse.go .hv-glow{animation:hvGlow .38s ease-out forwards}
@keyframes hvRing{0%{opacity:.95;width:52px;height:52px;border-width:2px}
                  100%{opacity:0;width:230px;height:230px;border-width:1px}}
@keyframes hvGlow{0%{opacity:.42;width:52px;height:52px}100%{opacity:0;width:190px;height:190px}}
@media(prefers-reduced-motion:reduce){
  .hv-pulse.go .hv-ring,.hv-pulse.go .hv-glow{animation:hvFlash .3s ease-out forwards}
  @keyframes hvFlash{0%{opacity:.6}100%{opacity:0}}}
.searchtags .strow .strow-ctl button{min-width:34px;min-height:34px;display:flex;align-items:center;justify-content:center;
  /* no divider rule: it was the inner wall of the box that just came off, and a floating
     vertical hairline with nothing enclosing it reads as debris. The 34px target and the
     hover fill are what separate these now. */
  background:none;border:0;color:var(--faint);cursor:pointer;font-size:12px;line-height:1;padding:0}
.searchtags .strow .strow-ctl button:hover{color:var(--ink);background:var(--raised)}
.searchtags .strow .strow-ctl .up.on{color:var(--good)} .searchtags .strow .strow-ctl .down.on{color:var(--bad)}
.searchtags .strow .strow-ctl .st-ok:hover{color:var(--good)} .searchtags .strow .strow-ctl .st-x:hover{color:var(--bad)}
/* ⚠ THE BAR-COLOUR AND NAME-COLOUR OVERRIDES FOR community/ai ARE DELETED (2026-08-04d). They
   predate the state system and they contradicted it out loud: `.ai` forced the bar to --hair
   (grey) and the name to --dim, and `.community` forced both to --info (blue) — three
   different things painting over the kind on the one surface where the bar is now SUPPOSED to
   be the kind. Colour says what a tag IS; style and type say where it came from. The dotted
   underline stays: that is the ai mark in the line channel, not a colour.
   nsfw/nsfl keep their bar override, and only they do — a content warning outranks a kind,
   which is the same floor "THE LAST WORD ON TAG COLOUR" enforces at the end of this file. */
.searchtags .strow.tag.nsfw{border-left-color:var(--cat-nsfw)}
.searchtags .strow.tag.nsfl{border-left-color:var(--cat-nsfl)}
.searchtags .strow.st-removed{opacity:.5} .searchtags .strow.st-removed .nm{text-decoration:line-through}
.searchtags .stmore{padding:5px 7px;font-size:10px;letter-spacing:calc(.06em * var(--track));color:var(--dim);border-top:1px solid var(--hair)}
.searchtags .stmore:hover{color:var(--accent)}

/* DOT BAR (owner ruling 2026-07-19o) — compact top-right chip of colored dots on a
   thumb card, replacing the old corner text badges. Dark rounded pill; tight ~10px
   dots. pointer-events:none so it never blocks a video control / tile tap. Category
   colors come from the --cat-* vars (pink=nsfw, red=nsfl); media stays house orange. */
.dotbar{position:absolute;top:6px;right:6px;z-index:2;display:flex;align-items:center;gap:4px;
  padding:3px 5px;border-radius:8px;background:rgba(10,11,13,.72);border:1px solid rgba(255,255,255,.06);pointer-events:none}
/* ⚖ THE MARK IS A DRAWING NOW, NOT A DISC (owner ask 2026-08-25: *"perhaps we turn the colored
   dots into colored icons? helps colorblind as well"*). Four states told apart by HUE ALONE is
   the worst case for deuteranopia — photographed at 390x844 through the matrix, orange, pink
   and red become three identical olives — so each carries a silhouette from DOT_ICON as well.
   ⛔ THE COLOURS DO NOT MOVE: they are learned, and they are still the fastest read for anyone
   who can see them. They are `color` rather than `background` now, because the mark inside is
   drawn in currentColor — one property, one place, and the skins keep overriding the same vars.
   ⚠ AND THE BOX DOES NOT GROW. 10px was measured on the real grid before the drawings were
   trusted (the bar is 64px on a 178px tile); a mark that needed 14px would have cost the tile
   a sixth of its width, and that would have been the owner's call rather than a quiet edit.
   ⚠ THE GROUND: the discs carried a 1px dark ring so they held on a bright thumbnail. A hairline
   around a padlock would be noise, so the same job is done by a drop-shadow that follows the
   ink — the bar's own dark pill does the rest. */
/* ⚠ SCOPED UNDER `.dotbar` NOW, AND THAT IS A FIX RATHER THAN TIDYING. `.cdot` IS TWO THINGS
   in this client: this marker, and the `·` SEPARATOR in a comment metaline (page-post-comments.js,
   page-s-thread.js). s-forum.css:738 records the collision in full — every separator on `/s` was
   being drawn as a 10px ringed blob until it was overridden there, scoped, with a note that the
   identical bug on `/post` was out of that pass's scope. An unscoped rule here is the half of
   that bug nobody has been able to reach; a scoped one cannot leak onto a character again. */
.dotbar .cdot{width:10px;height:10px;display:block;filter:drop-shadow(0 0 1px rgba(0,0,0,.85))}
.dotbar .cdot > svg{width:100%;height:100%;display:block}
.dotbar .cdot.dot-media{color:#ff9e3d}
.dotbar .cdot.dot-nsfw{color:var(--cat-nsfw)}
.dotbar .cdot.dot-nsfl{color:var(--cat-nsfl)}
/* VIOLET for private and TEAL for niche — neither is a --cat-* colour, so the two marks that
   are NOT content warnings can never be mistaken for one. ⚖ AND THE TWO ARE FAR APART IN THE
   BLUE CHANNEL as well as in hue, which is what keeps them separable when the red/green axis
   is gone: violet and teal are the pair that survives the deuteranopia matrix best. */
.dotbar .cdot.dot-private{color:#b58cff}
.dotbar .cdot.dot-niche{color:#4fd0c4}
/* the thumb containers must position the bar (tile is already relative). */
.hotthumb{position:relative}
.cand{position:relative}

/* HOVER VIDEO PREVIEW (owner ruling 2026-07-19p) — the lazy-created <video>/<img>
   overlays the thumb (object-fit:cover, seamless from the poster/thumb). z-index 1 so
   the dot bar (z-index 2, pointer-events:none) stays visible over the playing preview. */
/* hover preview: .hoverpreview is the OVERLAY CONTAINER (just the media — see
   HOVER CARD below for the scrub bar/volume/action controls, which live
   OUTSIDE it as their own floating elements). killHoverPreview removes the
   container in one shot. */
/* ══ THE TAG HOVER CARD (owner ruling 2026-07-31m) ═════════════════════════════════════
   The FILL is the dwell timer made visible: a bar crossing the chip in its own kind colour,
   and the instant it reaches the right edge the card opens. The post card's dwell is an
   invisible wait; this one tells you it is coming.
   ⚠ The fill sits UNDER the text (z-index 0 against the chip's own content) and is
   pointer-events:none, so it can never eat the click that the chip already had. */
.tagfill{position:absolute;left:0;top:0;bottom:0;width:0;pointer-events:none;z-index:0;
  background:currentColor;opacity:.22;
  transition:width var(--tagfill-ms, .62s) linear}
/* ⚠ `:not(.tagfill)` IS LOAD-BEARING. This rule lifts a chip's real content above the fill,
   but `.tagcloud a>*` (0,1,1) also outranks `.tagfill` (0,1,0) — so without the exclusion it
   forced the fill to position:relative, and a relative <span> is INLINE, and `width` does not
   apply to a non-replaced inline element. The bar computed to 0px and never moved. */
.tag>*:not(.tagfill),.tagcloud a>*:not(.tagfill),.vw-tag>*:not(.tagfill){position:relative;z-index:1}
@media(prefers-reduced-motion:reduce){
  /* no crawl — the card still opens on the same clock, the bar just does not animate */
  .tagfill{transition:none;opacity:.14}
}
/* ⚠ ONE RUNG BELOW `.hc-rig`, DELIBERATELY. The post hover card can be opened FROM INSIDE
   this one (the post strip is armed with the grid's own hoverPreview), so it has to land on
   top. Equal z-index would leave that to DOM order — which happens to work today and would
   silently invert the day either element is appended somewhere else. */
.tagcard{position:fixed;z-index:calc(var(--z-hovercard) - 1);width:min(420px,calc(100vw - 16px));
  max-height:min(70vh,560px);overflow-y:auto;overscroll-behavior:contain;
  background:var(--sunk);border:1px solid currentColor;padding:12px 14px;
  opacity:0;transform:translateY(-4px);transition:opacity .14s ease,transform .14s ease;
  box-shadow:0 10px 40px rgba(0,0,0,.6)}
.tagcard.on{opacity:1;transform:none}
.tagcard-head{display:flex;align-items:baseline;gap:9px;flex-wrap:wrap;
  padding-bottom:8px;border-bottom:1px solid var(--hair)}
.tagcard-name{font-size:15px;letter-spacing:calc(.03em * var(--track))}
.tagcard-kind{font-size:9px;letter-spacing:calc(.16em * var(--track));text-transform:var(--caps);
  color:var(--faint)}
.tagcard-count{margin-left:auto;font-size:10.5px;color:var(--accent);text-decoration:none;
  border-bottom:1px dotted var(--accent-deep,var(--accent));white-space:nowrap}
.tagcard-count:hover{color:var(--bright);border-bottom-color:var(--bright)}

/* THE POPUP FRAME (owner ruling 2026-08-06) — the same card body the hover uses, centred over
   a backdrop because a phone has no pointer for it to sit beside. Everything about the CARD is
   inherited; only position/size are overridden, so a change to .tagcard reaches both surfaces.
   The 44px close target is this file's law for anything a thumb must hit. */
.tagcard-pop{position:relative;left:auto;top:auto;width:min(460px,calc(100vw - 28px));
  max-height:min(78vh,720px);overflow-y:auto;-webkit-overflow-scrolling:touch}
.tagpop{display:flex;align-items:center;justify-content:center;padding:14px}
.tagpop-x{position:absolute;top:6px;right:6px;min-width:44px;min-height:44px;background:none;
  border:0;color:var(--dim);font:inherit;font-size:15px;line-height:1;cursor:pointer;z-index:1}
.tagpop-x:hover{color:var(--bright)}
/* The head must not run under the ✕ — the count is a TAP TARGET on this surface, and a target
   sitting beneath another target is the one collision a thumb cannot resolve. */
.tagcard-pop .tagcard-head{padding-right:44px}
.tagcard-desc{color:var(--ink);font-size:12px;line-height:1.55;margin-top:9px}
.tagcard-wiki{display:flex;gap:10px;margin-top:10px;padding-top:9px;border-top:1px solid var(--hair);
  color:var(--dim);font-size:11.5px;line-height:1.5}
.tagcard-wikithumb{width:64px;height:64px;object-fit:cover;flex:0 0 auto;border:1px solid var(--hair)}
.tagcard-wikitext{min-width:0}
.tagcard-wikititle{color:var(--ink);font-size:12px;margin-bottom:3px}
.tagcard-rel{display:flex;flex-wrap:wrap;gap:5px;margin-top:10px}
/* ⚠ ONE ROW, like the inspector's strip — a card that grows with the tag's post count would
   cover the page on `music`. */
/* ⚠ `contain` ON BOTH AXES, not just x. A VERTICAL wheel over this strip cannot scroll it,
   so without the y containment the gesture chains straight past the card to the page —
   which then scrolls the chip out from under a position:fixed card. */
.tagcard-posts{display:flex;flex-wrap:nowrap;gap:4px;overflow-x:auto;margin-top:10px;
  padding-top:9px;border-top:1px solid var(--hair);overscroll-behavior:contain}
.tagcard-shot{flex:0 0 auto;line-height:0;border:1px solid var(--hair);position:relative}
.tagcard-shot img{width:46px;height:46px;object-fit:cover;object-position:top;display:block}
.tagcard-shot.veiled img{filter:blur(9px)}
.hoverpreview{position:absolute;inset:0;z-index:1;background:var(--sunk)}
.hoverpreview .hp-media{width:100%;height:100%;object-fit:cover;display:block}

/* RIG (2026-07-21t) — the ONE element that actually receives the grow+tilt
   transform. A transformed element establishes a new containing block for
   position:fixed/absolute DESCENDANTS (real CSS behavior) — so the card,
   scrub bar, and footer below all live INSIDE this, each still using the
   exact same absolute pixel coordinates they always did (the rig itself
   sits at fixed viewport 0,0 and contributes no base offset), but now they
   translate/scale/rotate together as ONE rigid object instead of the card
   tilting alone while the bar and buttons stayed flat. */
/* THE RANK PIP (owner ruling 2026-07-27) — the tier number, 1..10, in a circle in front of
   every username. It wears the same rk-<tier> class as the name, so colour and digit agree;
   the digit is what makes the tier readable without having learned the palette.
   currentColor throughout, so it inherits whatever glow the rank class already sets rather
   than duplicating ten colour rules. Sized in em so it tracks the text it sits against —
   the same pip works in an h1 plaque name and in a comment byline. */
/* THE SPINE ROW (owner ruling 2026-07-27): label · the value it ADDS · its share. The value
   is pushed right against the percentage so the eye reads label -> number -> share, and the
   three numbers form a column that sums to the score above them. */
.spine-add{margin-left:auto;margin-right:.6em;font-size:11px;color:var(--bright);opacity:.95}
.spine-pct{opacity:.55;min-width:3.2em;text-align:right}
/* stats-tab section header: the RAW count, then the modifier that is applied to it. */
.pillar-detail .pillar-total{margin-left:auto;font-variant-numeric:tabular-nums;opacity:.9;font-size:13px}
.pillar-detail .pillar-mod{margin-left:.45em;opacity:.5;font-size:12px;text-transform:none;letter-spacing:0}
.rank-pip{display:inline-flex;align-items:center;justify-content:center;
  /* MIN-width, not width, and a fully-round radius: tiers 1-9 stay a true circle (the box is
     square at min-width == height) while CELESTIAL's two digits push it into a short pill
     rather than spilling out of a fixed circle, which is what a hard width did. */
  min-width:1.55em;height:1.55em;padding:0 .32em;margin-right:.4em;border-radius:999px;
  border:1px solid currentColor;box-sizing:border-box;
  font-size:.62em;line-height:1;font-variant-numeric:tabular-nums;
  vertical-align:middle;flex:0 0 auto;opacity:.9}
.prof-name .rank-pip{font-size:.42em;vertical-align:.28em}
/* a link is inline, so the pip must not stretch the line box on a wrapped byline */
.ulink .rank-pip{transform:translateY(-.05em)}
.pillar-detail .prof-lbl{display:flex;align-items:baseline;justify-content:space-between;gap:10px}
/* the section's contribution to the score, right-aligned against its label. The lines below
   it are plain counts (owner ruling 2026-07-27), so this is the ONE number in the panel that
   is in score units — and the three of them sum to the score at the top of the page. */
/* THE RIG IS THE ONLY .hc-* ON THE PAGE LADDER. It is position:fixed WITH will-change —
   two separate reasons it opens its own stacking context — so every number on a .hovercard /
   .hc-gap / .hc-foot / .hp-progress / .hc-paused below is INTERNAL to it and can never
   compete with anything on <body>. They used to read 250/300/301, which looked like page
   ranks and encouraged exactly that misreading (the .pickerlay note upstairs even quoted "the
   rig tops out at 301" as if 301 were a page-level number). They are 1..4 now. Same order,
   same pixels, no false claim. */
.hc-rig{position:fixed;left:0;top:0;z-index:var(--z-hovercard);will-change:transform}

/* HOVER CARD v1 (prototype, owner idea 2026-07-21k) — the small in-tile preview
   GROWS into this: a floating card, sized to the post's OWN aspect ratio (JS sets
   left/top/width/height inline — the BASE resting layout, with the grow+tilt
   animation living entirely on the RIG's transform, never touching these).
   The graduated .hoverpreview fills it exactly like it filled the tile; only
   object-fit flips from cover to contain, since the WHOLE point is showing
   the real, uncropped aspect ratio. */
.hovercard{position:absolute;border-radius:10px;overflow:hidden;
  box-shadow:0 20px 60px rgba(0,0,0,.55),0 2px 10px rgba(0,0,0,.45);background:#000;display:block}
.hovercard .hoverpreview{position:absolute;inset:0}
.hovercard .hp-media{object-fit:contain;background:#000}
/* the tilt IS the motion this feature is about — reduced-motion never grows the
   card at all (hoverPreview's HOVER_CAPABLE+reduceMotion gate covers this file's
   whole feature, small preview included), so no separate override is needed here. */

/* GAP CONNECTOR (2026-07-21u) — invisible, just occupies the dead zone
   between the card (or scrub bar) and the footer so the grid tile underneath
   is never hit-testable there (see growHoverCard for why that mattered:
   a competing tile's mouseenter would unconditionally kill this preview). */
.hc-gap{position:absolute;z-index:3;background:transparent}   /* inside .hc-rig — see there */

/* SCRUB BAR (restored + relocated, 2026-07-21s): video only, built fresh in
   growHoverCard (the small preview never shows one — see hoverpreview above).
   A SEPARATE floating pill straddling the card's bottom edge (JS centers its
   top on cardY+h) rather than a child of .hovercard, which clips at its own
   rounded-corner box via overflow:hidden and would cut off anything meant to
   hang past it. z-index above the card (within the SAME rig) so it reads
   clearly at the seam.
   .hp-progress is the HIT TARGET (HOVERCARD_PROG_HIT_H tall, invisible) —
   .hp-progress-track is the actual visible pill, centered within it and much
   thinner (2026-07-21v: a hitbox the same size as the visible bar was too
   easy to miss, landing on the card underneath and navigating to the post
   instead of scrubbing). */
.hp-progress{position:absolute;z-index:4;cursor:pointer;display:flex;align-items:center}
.hp-progress-track{width:100%;border-radius:999px;overflow:hidden;
  background:rgba(0,0,0,.5);box-shadow:0 1px 4px rgba(0,0,0,.4)}
.hp-progress-fill{width:100%;height:100%;background:var(--accent);transform:scaleX(0);
  transform-origin:left;transition:transform .12s linear}
@media(prefers-reduced-motion:reduce){.hp-progress-fill{transition:none}}

/* FOOTER (restored + relocated, 2026-07-21s): volume (video only) + upvote/
   downvote/save, in a strip BELOW the card — never overlaid on the media,
   the original placement that turned out to be unusable in the ~1s window
   before the grow used to start. A plain sibling <div> in the same rig (not
   nested inside the card's <a>), so its buttons need no navigation-guard
   stopPropagation. */
/* ── THE POSTER ROW ABOVE THE CARD (owner ask 2026-07-30) ─────────────────────────────────
   *"the posters username and pfp above the desktop card. The same way it renders on the
   mobile version."* So it is the mobile `.vw-poster` row's geometry — 36px chip, 8px gap,
   name beside it — sitting above the card instead of over the media.

   ⚠ IT GETS A PLATE AND THE MOBILE ONE DOES NOT, and that is not an inconsistency. The
   viewer's poster row sits ON the media and is made legible by `.vw-top`'s gradient scrim;
   this one floats over the GRID, which is a wall of arbitrary thumbnails with no scrim
   anywhere. Same plate as `.hc-foot`, so the card reads as one object with a strip above and
   a strip below rather than a bar and a loose caption.

   ⚠ AND STILL NO text-shadow ON THE NAME — same reason as the note on `.vw-poster` below:
   `.ulink.apex-name` is background-clip:text with color:transparent, so a shadow under it
   paints a silhouette of unfilled glyphs. Only box properties here. */
.hc-poster{position:absolute;z-index:3;display:flex;align-items:center;gap:8px;
  padding:4px 10px;border-radius:8px;background:rgba(12,14,18,.86);
  backdrop-filter:blur(4px);border:1px solid var(--hair-hi);box-sizing:border-box;
  overflow:hidden}
.hc-poster:empty{display:none}
.hc-poster-av{flex:0 0 auto;display:flex;align-items:center;justify-content:center;
  text-decoration:none}
.hc-poster .vw-cav{width:30px;height:30px}
.hc-poster-who{flex:0 1 auto;min-width:0;display:inline-flex;align-items:center;
  text-decoration:none;font-size:13px;overflow:hidden;white-space:nowrap;
  text-overflow:ellipsis}
/* THE MESSAGE DOOR (DM_DESIGN A3) — the poster row's one verb, flush right.
   ⚖ IT SPEAKS `.hc-open`'S LANGUAGE, NOT `.hc-act`'S, and that is the anatomy rule rather
   than a preference: the card already has two vocabularies — a row of 30px icon-only circles
   in the footer and ONE hairline box carrying a word in the middle of it — and a labelled
   verb belongs to the second. An icon here would be a fifth glyph nobody has a name for, and
   a spaced-mono-caps label would make this the only shouting text on a card whose own
   username sits at 13px.
   ⚠ `flex:0 0 auto` beside the name's `0 1 auto`: a long username ellipsises, the verb does
   not — a door that disappears at 24 characters is not a door.
   ⚠ NO width/height. `.hc-act` is 30x30 and lives under mobiletaps.test.js's census; this is
   sized by its padding, so it has no floor to trip and nothing to waive. The card is desktop
   only anyway (HOVER_CAPABLE), which is exactly why it may not become the only door. */
.hc-msg{flex:0 0 auto;margin-left:auto;display:inline-flex;align-items:center;
  padding:4px 10px;border-radius:6px;border:1px solid var(--hair-hi);
  background:rgba(255,255,255,.06);color:var(--ink);text-decoration:none;
  font-size:12px;line-height:1;white-space:nowrap}
.hc-msg:hover{background:rgba(255,255,255,.14);border-color:var(--accent);color:var(--accent)}

.hc-foot{position:absolute;z-index:3;display:flex;align-items:center;justify-content:space-between;
  gap:10px;padding:8px 10px;border-radius:8px;background:rgba(12,14,18,.86);
  backdrop-filter:blur(4px);border:1px solid var(--hair-hi);box-sizing:border-box}
.hc-vol{display:flex;align-items:center;gap:6px}
.hc-mute{background:none;border:0;color:var(--accent);cursor:pointer;font-size:14px;line-height:1;
  padding:0;width:22px;height:22px;display:flex;align-items:center;justify-content:center}
.hc-mute:hover{color:var(--accent-hi,var(--accent))}
/* ⚑ THE HOVER CARD'S SPEAKER IS DRAWN NOW (forage-jx64.33) — `hcMuteMark()` in common-hover.js,
   MEDIA_ICON's stroked pair, the same mark and the same accessor as the masthead's global volume
   button. ⚠ THE BOX IS THE CALLER'S: `mediaIcon` stamps `.navi`, whose 14px box (app.css:489) is
   sized for the MASTHEAD's 10.5px nav row beside 10.5px type — not for a 22px button standing on
   its own next to a slider. BOTH WERE PHOTOGRAPHED at 2x in the running app (.scratch/tour-fix,
   `hover-after-1-muted-16px` / `-2-muted-14px-unboxed`): at 14 the cross of the `mute` cut is a
   0.95px stroke and crowds the cone, and the mark reads small against the 22px box it is centred
   in; 16 is where the two separate. That is the same measurement the DM reader's pair mark is
   filed under (app-admin.css `.as-note .as-pair`, 15px and not the pills' 13px) — a mark that
   stands alone needs more than one sitting in a labelled row. 16 is also the masthead volume
   button's own ratio (18 in a 30px button) restated at this size.
   ⛔ Not a `font-size`: the glyph this replaced took its size from `.hc-mute`'s, and an <svg>
   ignores it — leaving that line as the only size here is how this ships 100% wide. */
.hc-mute .navi{width:16px;height:16px;vertical-align:0}
.hc-volslider{appearance:none;-webkit-appearance:none;width:70px;height:16px;margin:0;background:transparent;cursor:pointer}
.hc-volslider::-webkit-slider-runnable-track{height:3px;border-radius:2px;background:rgba(255,255,255,.28)}
.hc-volslider::-webkit-slider-thumb{-webkit-appearance:none;width:9px;height:9px;margin-top:-3px;border:0;border-radius:50%;background:var(--accent)}
.hc-volslider::-moz-range-track{height:3px;border-radius:2px;background:rgba(255,255,255,.28)}
.hc-volslider::-moz-range-thumb{width:9px;height:9px;border:0;border-radius:50%;background:var(--accent)}
/* OPEN-THE-POST sits in the TRUE middle of the bar, absolutely centred rather than laid
   out between its neighbours: a video card has a volume group on the left and an image
   card does not, so a flex position would drift depending on the media type. Centre is
   where the owner put it in the sketch and it should stay there. */
.hc-open{position:absolute;left:50%;transform:translateX(-50%);display:flex;
  align-items:center;justify-content:center;width:92px;height:30px;border-radius:6px;
  border:1px solid var(--hair-hi);background:rgba(255,255,255,.06);color:var(--ink);
  text-decoration:none;cursor:pointer}
.hc-open:hover{background:rgba(255,255,255,.14);border-color:var(--accent);color:var(--accent)}
/* PAUSED BADGE: a video with no chrome that simply stops looks broken rather than paused.
   Shows what the NEXT click does, which is the honest thing for a toggle to advertise. */
/* 2 = over .hoverpreview (1), under .hc-gap/.hc-foot (3) and .hp-progress (4). It is a child
   of .hovercard, which sets no z-index, so it competes in the RIG's context alongside them. */
.hc-paused{position:absolute;z-index:2;left:50%;top:50%;transform:translate(-50%,-50%) scale(.9);
  width:64px;height:64px;border-radius:50%;display:flex;align-items:center;justify-content:center;
  font-size:26px;color:#fff;background:rgba(0,0,0,.55);border:1px solid rgba(255,255,255,.35);
  opacity:0;pointer-events:none;transition:opacity .12s ease,transform .12s ease}
.hc-paused.on{opacity:1;transform:translate(-50%,-50%) scale(1)}
@media (prefers-reduced-motion: reduce){ .hc-paused{transition:none} }
/* margin-left:auto, NOT the bar's space-between: an image card has no volume group, which
   left the actions as the ONLY flex child — and a lone child under space-between sits at
   the START. Star and share ended up on the left of an image card and on the right of a
   video one. Pushed right explicitly, they land in the same place on every card, whatever
   else the bar happens to contain (a self post has no up/down either). */
.hc-acts{display:flex;gap:6px;margin-left:auto}
.hc-act{width:30px;height:30px;border-radius:50%;border:1px solid var(--hair-hi);
  background:rgba(255,255,255,.06);color:var(--ink);
  font-size:14px;line-height:1;cursor:pointer;display:flex;align-items:center;justify-content:center}
.hc-act:hover{background:rgba(255,255,255,.12);border-color:var(--accent)}
.hc-up.on{color:var(--good);border-color:var(--good)}
.hc-down.on{color:var(--bad);border-color:var(--bad)}
.hc-save.on{color:var(--accent);border-color:var(--accent)}


/* ═══ THE AUTOCOMPLETE'S THREE BANDS (owner 2026-08-10) ══════════════════════════════════
   A row that APPLIES an existing tag and a row that MINTS one look identical if both are just
   a name — and that difference is the point: one is free, the other adds a word to the
   archive's vocabulary permanently. `new` is the only row that gets the accent, because it is
   the only row that creates something; `near` is recessive because it is a correction being
   offered, not the thing you asked for. */
.ac div.ac-new .ac-tag-new{color:var(--accent);letter-spacing:calc(.04em * var(--track))}
.ac div.ac-new{border-top:1px dashed var(--hair-hi)}
.ac div.ac-near{opacity:.82}
.ac div.ac-near .c{color:var(--faint);font-style:italic}

/* the `+term` PROPOSE strip on the grid. Quiet by default — it is an offer, not a result. */
.proposebar{display:flex;flex-wrap:wrap;gap:8px 16px;align-items:center;margin:0 0 12px;
  padding:8px 10px;border:1px dashed var(--hair-hi);background:var(--sunk)}
.proposeitem{display:inline-flex;align-items:center;gap:9px;font-size:12px}
.proposeitem .btn{padding:3px 9px;font-size:10.5px}

/* a PROPOSED tag's row on a tile: the same shape as a real searched-tag row, but visibly not
   yet a fact about this post — dashed left edge, muted name, and the net replaced by the
   reason it has none. The ▲ is the ordinary one, because it means the ordinary thing. */
.strow.st-propose{border-left:2px dashed var(--hair-hi);opacity:.9}
.strow.st-propose .nm{color:var(--dim)}
.strow.st-propose .st-suggesting{color:var(--faint);font-style:italic}
.strow.st-propose.st-done .nm{color:var(--good)}
/* the propose control: looks like the ▲ because it means "this belongs here", but it is not an
   applied-tag vote and does not wear that class — see proposeTagRow in common-tiles.js. */
.strow.st-propose .st-add{background:none;border:1px solid var(--hair-hi);color:var(--dim);
  cursor:pointer;font:inherit;font-size:12px;line-height:1;padding:2px 8px;border-radius:2px}
.strow.st-propose .st-add:hover{color:var(--good);border-color:var(--good)}
.strow.st-propose .st-add:disabled{opacity:.5;cursor:default}

/* ══ /tags' TAG-INFO PANEL REACHES THE FLOOR (forage-e88, 2026-08-21) ═════════════════════════
   ⚑ AND IT LIVES HERE, NOT BESIDE THE RULES IT LIFTS. Every `.tagi-*` rule is in web/app.css,
   which was AT the 1,000-line ceiling (repealed 2026-08-29) — the bead says so itself and names the two ways out
   ("pay for the lines by compacting, or put the coarse block in a later part"). This is a later
   part: `tags.html` links app.css then app-tagrows.css, so a plain property like `gap` wins from
   here, and it is the sheet that already owns tag chrome. ⛔ Do not move it back into app.css to
   be tidy; that pushed a 999-line file over the then-live ceiling for no behaviour.

   EVERY NUMBER BELOW IS MEASURED, in the real app at 390x844 with touch, on a throwaway
   Postgres with real tags, with a tag actually open. Two different repairs, because two
   different KINDS of control are in this panel:

   ⚖ 1. THE FORM ROWS TAKE A REAL min-height, and they have to. `.tagi-search input` (178x38),
   `.tagi-link input` (250x38 and 110x38) and `.tagi-sel` (139x38) are REPLACED elements — an
   `::after` on an `<input>` or a `<select>` is not rendered at all, so the hit-area shape is
   unavailable by construction. It is the SAME 6px round-up `.authcard input,.authcard
   button.go` took in forage-69n, on the same reasoning, and these are UA-centred so min-height
   alone keeps the text in the middle. `.tagi-plus` (40x38), `.tagi-mag` (40x38) and
   `.tagi-linkbtn` (75x38 / 66x38) go with them rather than taking an overhang: they are INLINE
   WITH those inputs, the row has to grow 6px for the input regardless, and a button left drawn
   38px in a 44px row is a worse picture than the 6px. One row, once, 6px.

   ⚖ 2. THE TYPE LIST TAKES THE GAP FIRST AND THEN A HIT AREA. `.tagi-type` is 62-103 x 34,
   SEVEN of them, and at 390px they WRAP into two rows (measured y=89 and y=128, a 39px pitch)
   — `.tagi-typelist` is `gap:5px` in BOTH axes and the tightest pair on the whole page is
   `general` <-> `character` at 5.0px. ⚑ THE BEAD ASKED FOR 5 -> 8 AND 8 IS TWO PIXELS SHORT:
   34 -> 44 is 5px of overhang each way, so an 8px row gap leaves the two hit boxes OVERLAPPING
   by 2px and a thumb aiming at `media` can land on `general`. 10px makes them tile exactly, and
   costs 5px on the whole panel because there is only one wrap. ⚠ ROW GAP ONLY — the COLUMN gap
   stays 5px and the overhang stays VERTICAL, because sideways these chips are 5px apart and a
   horizontal halo would put `character` under a thumb aiming at `general`, which is the mis-tap
   the gap number exists to measure.

   ⚖ 3. `.tagi-name` (50x32, the tag's own name button in the header) and `.tagi-treeh` (332x38,
   the tree's section lid) are text controls whose rows are load-bearing — the header row and
   the lid's own hairline — so both take the vertical overhang and neither changes size.
   `.tagi-treebtn` (26px, the tree's +/-/⊙) goes with them; ⚠ it is the ONE control in this bead
   NOT photographed, because the tree is collapsed by default and nothing in the harness opens
   it — recorded rather than claimed. */
@media(pointer:coarse){
  .tagi-search input,.tagi-link input,.tagi-sel,
  .tagi-plus,.tagi-mag,.tagi-linkbtn{min-height:var(--tap-min)}
  .tagi-typelist{gap:10px 5px}
  .tagi-name,.tagi-type,.tagi-treeh,.tagi-treebtn{position:relative}
  .tagi-name::after,.tagi-type::after,.tagi-treeh::after,.tagi-treebtn::after{
    content:"";position:absolute;left:0;right:0;top:50%;transform:translateY(-50%);
    height:var(--tap-min)}
}
