/* app-curation.css — crowd tags, the invite branch view and the genealogy tree.

   ⚖ 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 1546-1829). 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. */
/* ================= crowd tags v1 (three-zone panel + verdicts + rep) ========= */
.tagzones .tz{margin-top:10px}
.tz-h{font-size:9px;letter-spacing:calc(.2em * var(--track));text-transform:var(--caps);color:var(--faint);
  border-bottom:1px solid var(--hair);padding-bottom:3px;margin-bottom:7px}
.tz-h::before{content:"◈ ";color:var(--accent-deep)}
/* community (provisional) applied tag: distinct cyan mark + live net */
.tag.community{color:var(--info);border-color:var(--hair-hi);border-style:solid}
.tag.community a{color:inherit}
.tag .tnet{color:var(--faint);font-variant-numeric:tabular-nums;letter-spacing:calc(.02em * var(--track))}
/* admin verdict buttons (sit inside a tag chip or a queue/suggestion row) */
.verdicts{display:inline-flex;gap:3px;margin-left:5px}
.vbtn{border:1px solid var(--hair);background:none;color:var(--dim);cursor:pointer;
  font-size:10px;line-height:1;padding:2px 5px;letter-spacing:calc(.04em * var(--track))}
.vbtn:hover{border-color:currentColor}
.vbtn.reinforce{color:var(--good)} .vbtn.disapprove{color:var(--bad)}
.vbtn.punish{color:var(--accent)} .vbtn.revert{color:var(--info)}
/* yes/no vote controls */
.votes{display:inline-flex;gap:2px;margin-left:5px}
.vote{border:1px solid var(--hair);background:none;cursor:pointer;color:var(--faint);
  font-size:10px;line-height:1;padding:2px 5px}
.vote:disabled{opacity:.5;cursor:default}
.vote.yes:hover,.vote.yes.on{color:var(--good);border-color:var(--good)}
.vote.no:hover,.vote.no.on{color:var(--bad);border-color:var(--bad)}
.applied-votes{margin-left:6px}
/* THE MODIFIER-VERB COLOURS MOVED TO THE END OF THIS FILE — see "THE LAST WORD ON
   MODIFIER VERBS". They lost a cascade fight here; the reason is written down there. */
/* While a modifier is down, the whole tag row reads as "armed" so the mode is visible even
   for rows scrolled past the cursor. Cheap: one class on <body>, no per-row work. */
body.mod-shift .tagrows .tag,body.mod-ctrl .tagrows .tag{transition:none}
body.mod-shift .applied-votes,body.mod-ctrl .applied-votes{outline:1px dotted var(--hair-hi);outline-offset:2px}

/* ---- THE MODE DIAL (owner request 2026-07-31b) --------------------------------------
   The mobile half of the modifier verbs: hold the corner button, slide up a vertical bar,
   release on a mode. Stops read UPWARD from the thumb — vote, reinforce/remove, punish — so
   the gesture's null result (release without moving) is the harmless one and the most
   destructive verb is the furthest to reach. */
.mdial{position:fixed;z-index:calc(var(--z-fab) + 1);max-width:calc(100vw - 16px);
  display:flex;flex-direction:column;gap:4px;padding:6px;
  background:var(--panel);border:1px solid var(--hair-hi);border-radius:10px;
  box-shadow:0 8px 28px rgba(0,0,0,.55);pointer-events:none}
.mdial-stop{display:flex;align-items:center;gap:8px;min-width:170px;min-height:44px;
  padding:6px 10px;border:1px solid transparent;border-radius:7px;color:var(--faint);
  font-size:11px;letter-spacing:calc(.06em * var(--track));text-transform:var(--caps)}
.mdial-g{font-size:14px;line-height:1;min-width:26px;text-align:center}
.mdial-l{white-space:nowrap}
/* The HIGHLIGHTED stop wears the verb's own plate — the same three colours the desktop
   buttons use, so "green means reinforce" is one fact across both halves of the feature. */
.mdial-stop.on{color:var(--ink);border-color:var(--hair-hi);background:var(--raised)}
.mdial-stop.md-shift.on{color:var(--bg);background:var(--good);border-color:var(--good)}
.mdial-stop.md-ctrl.on{color:var(--bg);background:var(--accent);border-color:var(--accent)}

/* THE HOST BUTTON IS THE INDICATOR. A latch outlives the gesture that set it, so unlike a
   held key there is nothing else on screen saying "your next tap removes a tag". The button
   wears the mode until it changes. ::after (not textContent) so the host's own label — the
   search FAB's icon — is left alone. */
.md-armed{position:relative}
.md-armed::after{content:attr(data-md-glyph);position:absolute;top:-6px;right:-6px;
  min-width:20px;height:20px;padding:0 4px;border-radius:10px;
  display:flex;align-items:center;justify-content:center;
  font-size:10px;line-height:1;color:var(--bg);background:var(--good)}
.md-armed.md-ctrl::after{background:var(--accent)}
.md-armed.md-shift{border-color:var(--good)}
.md-armed.md-ctrl{border-color:var(--accent)}

/* The dial's own host, for pages with tag rows but no search FAB (queue, collective, post).
   Bottom-right, above the home-indicator inset, out of the way of the viewer's bottom bar.

   ⚖ IT IS A CELL, NOT A DISC (forage-qw2w.32, owner review 2026-09-05: *"it is the only circle
   in a square-bracket design"*). It was a 46px `border-radius:23px` puck — the ONE round object
   on a site whose every surface is a hard-cornered hairline box with corner brackets, which is
   why it read as somebody else's control dropped on top of ours. It now wears `.pn`'s OWN
   construction (app-panel.css:74): no radius, a hairline box, and four corner brackets drawn as
   EIGHT one-pixel gradient layers in one `background-image` — the same reason given there, that
   an L needs two rectangles and a corner that costs four spans is four spans on every panel.
   The arms are 9px (a 46px cell, against the panel's 11px on a 300px+ box) and sit 3px inside
   the border, so the button reads as the frame's bottom-right cell.
   ⛔ NOT a pseudo-element: `.md-armed::after` is the armed-mode badge and `::before` belongs to
   the glyph span, so the background-layer spelling is the only one that leaves both alone.

   ⚖ AND IT SITS IN THE CORNER OF THE FRAME, NOT IN THE MIDDLE OF THE READING COLUMN. `bottom`
   was a flat 74px on every surface — the clearance the /queue verdict bar needs — which on the
   screens that have no bar parked it two thirds of the way up the last panel, ON the text. The
   bead's own measurements: it covered the "o" of "5h ago" on /hot and the `tag name…` input on
   /tags. The lift is now a VARIABLE that only the surfaces which dock something to that edge
   pay for, and `--fab-lift` is set BY ABSENCE: the default is the old 74px, so a browser
   without `:has()` keeps exactly today's seat and nothing regresses on it.
   ⚖ AND THE BAND IT NOW SITS IN WAS ALREADY THERE, WHICH IS THE POINT. `body` has reserved
   `calc(72px + env(safe-area-inset-bottom))` of bottom padding since 2026-08-26, and app.css
   says in as many words what that padding is for: *"it reserves the search FAB's corner"*
   (`.wrap.pagecol::before` runs past the wrap by exactly it). The button was seated 74px up —
   two pixels ABOVE the top of its own reserved band — so at the end of every page it stood on
   the last panel instead of in the space the page had already cleared for it. 10px puts it back
   inside. ⛔ DO NOT ADD A SECOND BAND: a `padding-bottom` on `main.wrap` on top of body's is
   144px of dead space at the end of every phone page, which was written here and measured out
   again the same hour.
   ⚠ WHAT THIS DOES NOT FIX. A fixed control over a scrolling document has something behind it
   MID-SCROLL — that is what fixed means, and no seat measured (74px, 12px, 8px, flush) was free
   of every content box at rest. What the corner seat buys is that the thing behind it is the
   page's own reserved margin at the end, and the frame's edge rather than the middle of a card
   everywhere else. Removing the overlap outright needs either a reserved bottom RAIL (new
   chrome) or the control in flow (it stops being reachable from the top of a long page) — both
   the owner's design call, not a lane's. */
.mdial-fab{position:fixed;z-index:var(--z-fab);
  right:max(10px,env(safe-area-inset-right,0px));
  bottom:calc(env(safe-area-inset-bottom,0px) + var(--fab-lift,74px));
  width:46px;height:46px;min-width:46px;min-height:46px;padding:0;border-radius:0;
  border:1px solid var(--hair-hi);background-color:var(--panel);color:var(--faint);
  background-repeat:no-repeat;
  background-image:
    linear-gradient(var(--accent),var(--accent)),linear-gradient(var(--accent),var(--accent)),
    linear-gradient(var(--accent),var(--accent)),linear-gradient(var(--accent),var(--accent)),
    linear-gradient(var(--accent),var(--accent)),linear-gradient(var(--accent),var(--accent)),
    linear-gradient(var(--accent),var(--accent)),linear-gradient(var(--accent),var(--accent));
  background-size:9px 1px,1px 9px,9px 1px,1px 9px,9px 1px,1px 9px,9px 1px,1px 9px;
  background-position:
    3px 3px,3px 3px,
    calc(100% - 3px) 3px,calc(100% - 3px) 3px,
    3px calc(100% - 3px),3px calc(100% - 3px),
    calc(100% - 3px) calc(100% - 3px),calc(100% - 3px) calc(100% - 3px);
  font-size:13px;line-height:1;cursor:pointer;
  box-shadow:0 4px 16px rgba(0,0,0,.45);touch-action:none}
/* ⚖ THE LIFT IS SET BY ABSENCE, SO THE FAILURE MODE IS THE OLD SEAT (see above). `.verdictbar`
   (app-feeds.css:378) is the one thing this button has ever had to clear — pinned bottom on the
   queue and the collective, the surfaces mountFab() exists for. A surface that docks anything
   else to that edge joins this list rather than moving the button's own rule. */
body:not(:has(.verdictbar)){--fab-lift:10px}
/* ⚖ IT IS A SEARCH BUTTON (owner ruling 2026-08-03): a magnifier, drawn the same way
   `.xps-fab` draws it in xp-search.css — a currentColor mask over a label hidden by font-size:0,
   so the word survives for assistive tech. The two corners must read as ONE control; a member
   sees only this, an admin can also hold it. The armed-mode badge is `.md-armed::after`, a
   corner dot that sits ON TOP of the glyph and is unaffected. */
.mdf-g{pointer-events:none;font-size:0;line-height:1;display:block;width:22px;height:22px}
.mdf-g::before{content:"";display:block;width:22px;height:22px;background-color:currentColor;
  -webkit-mask-repeat:no-repeat;mask-repeat:no-repeat;
  -webkit-mask-position:center;mask-position:center;
  -webkit-mask-size:22px 22px;mask-size:22px 22px;
  -webkit-mask-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2.2' stroke-linecap='round'%3E%3Ccircle cx='10.5' cy='10.5' r='6.5'/%3E%3Cpath d='M20 20l-4.7-4.7'/%3E%3C/svg%3E");
          mask-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2.2' stroke-linecap='round'%3E%3Ccircle cx='10.5' cy='10.5' r='6.5'/%3E%3Cpath d='M20 20l-4.7-4.7'/%3E%3C/svg%3E")}
/* no mask support: show the word rather than an empty pill (mirrors xp-search.css) */
@supports not ((-webkit-mask-image: url("")) or (mask-image: url(""))) {
  .mdf-g{font-size:10px;width:auto;height:auto}
  .mdf-g::before{display:none}
}
.mdial-fab{display:flex;align-items:center;justify-content:center;color:var(--accent);
  border-color:var(--accent-deep)}
@media(prefers-reduced-motion:reduce){.mdial,.mdial-fab{transition:none}}
/* applied-tag self-delete / consensus removal: fly out (reduced-motion = instant) */
.tag{transition:opacity .28s ease,transform .28s ease}
.tag.tag-removing{opacity:0;transform:translateY(-4px) scale(.9)}
@media(prefers-reduced-motion:reduce){.tag{transition:none}.tag.tag-removing{opacity:0}}
/* the "suggested" (pending) zone rows */
.sugrow{display:flex;align-items:center;gap:8px;flex-wrap:wrap;padding:4px 0;
  border-bottom:1px dotted var(--hair);font-size:11px}
.sugname{color:var(--info)} .sugname:hover{color:var(--bright)}
.sugnet{color:var(--ink);font-variant-numeric:tabular-nums;min-width:34px}
.sugby{color:var(--faint);font-size:10px}
.tzmsg{margin-top:8px;min-height:1em}
/* the red "−N" penalty floater — negative twin of the +N juice */
.pointfloat.neg{color:var(--bad);text-shadow:0 1px 3px var(--void)}
.pointfloat.noanim{animation:none!important;opacity:1;transform:translate(-50%,-140%)}
/* reputation strip in the rail/profile dossier */
.dsr-rep{display:flex;flex-wrap:wrap;gap:6px;margin-top:9px}
.dsr-rep-chip{display:flex;align-items:baseline;gap:5px;font-size:10px;
  border:1px solid var(--hair);padding:2px 7px;letter-spacing:calc(.04em * var(--track))}
.dsr-rep-k{color:var(--dim);text-transform:var(--caps);font-size:8.5px;letter-spacing:calc(.12em * var(--track))}
.dsr-rep-v{color:var(--ink);font-variant-numeric:tabular-nums}
.dsr-rep-chip.warn{border-color:var(--bad)}
.dsr-rep-chip.warn .dsr-rep-v{color:var(--bad)}
/* /admin moderation queue cards */
.modcard{display:flex;gap:12px;border:1px solid var(--hair);background:var(--panel);
  padding:10px;margin-top:8px}
.modthumbwrap{flex:0 0 auto}
.modthumb{width:64px;height:64px;object-fit:cover;border:1px solid var(--hair-hi);background:var(--sunk)}
.modbody{flex:1;min-width:0;display:flex;flex-direction:column;gap:7px}
.modhead{display:flex;align-items:center;gap:10px;flex-wrap:wrap}
.modnet{color:var(--ink);font-variant-numeric:tabular-nums;font-size:11px}
.voterlist{display:flex;flex-wrap:wrap;gap:5px}
.voter{font-size:9.5px;letter-spacing:calc(.04em * var(--track));padding:1px 6px;border:1px solid var(--hair);color:var(--dim)}
.voter.yes{color:var(--good);border-color:var(--hair-hi)}
.voter.no{color:var(--bad);border-color:var(--hair-hi)}
.modacts{display:flex;gap:6px;flex-wrap:wrap}
.modacts .vbtn{font-size:10px;padding:4px 8px;position:relative}
/* ⚖ THE MODERATOR'S VERDICT ROW IS 20px AND ITS TARGET IS 44px (forage-2xcc, 2026-08-25).
   MEASURED in the real app at 390x844 with touch, /admin signed in as the owner with a real
   pending subShreddit request: `✓ approve` and `✕ decline` are **85.6x20, 6px apart**, with
   51px of clear space above and 33px below. A centred 12px overhang each way therefore reaches
   nothing — and VERTICAL ONLY, because 6px sideways is the tightest pair on the screen and a
   horizontal halo would put `decline` under a thumb aiming at `approve`.
   ⚑ WHY IT IS WORTH THE RULE. This is the same argument that lifted `.chip` one file over
   (mobiletaps.test.js, forage-7gu): a screen that creates subShreddits, reinforces, disapproves
   and PUNISHES is a mis-tap generator wherever it is opened, and every verdict here is an act on
   somebody else's account.
   ⚠ SCOPED TO `.modacts`, never bare `.vbtn`: `.verdictvote .vbtn` is already 56px and
   `.vote .vbtn` carries its own ::after in app-feeds.css. */
@media(pointer:coarse){
  .modacts .vbtn::after{content:"";position:absolute;left:0;right:0;top:50%;
    transform:translateY(-50%);height:var(--tap-min)}
}
@media(max-width:560px){
  .modcard{flex-direction:column}
  .sugrow{align-items:flex-start}
}
@media(prefers-reduced-motion:reduce){
  /* the penalty floater still SHOWS (so a penalty is never missed) but never
     animates — instant, static, then auto-removed. Overrides the blanket hide. */
  .pointfloat.neg.noanim{display:block!important}
}

/* ============================================================================
   INVITE BRANCH VIEW (invite_branch_view v1) — the lineage as a radial health
   map. The FACTORED starmap-precursor: SVG branch paths + DOM node chips over a
   terminal-dark field, pan/zoom. Every channel maps to a real number (thickness
   = strength, brightness = quality, red bleeds up = trouble, † = banned,
   HOLLOW = dormant — present but out of the running).
   ========================================================================== */
.bv-section{margin-top:16px}
.bv-bar{display:flex;align-items:center;gap:10px;flex-wrap:wrap;margin:8px 0}
.bv-ctl{border:1px solid var(--hair-hi);background:var(--raised);color:var(--accent);
  font:600 12px var(--ui);width:28px;height:26px;cursor:pointer}
.bv-ctl:hover{border-color:var(--accent)}
.bv-legend{display:flex;gap:14px;flex-wrap:wrap;font-size:10.5px;letter-spacing:calc(.04em * var(--track))}
.bv-legend span{white-space:nowrap}

.bv-stage{position:relative;height:min(70vh,620px);border:1px solid var(--hair);
  border-radius:2px;overflow:hidden;background:
    radial-gradient(120% 90% at 50% 40%, #0d1017 0%, #070809 70%, #050506 100%)}
.bv-viewport{position:absolute;inset:0;overflow:hidden;cursor:grab;
  touch-action:none;outline:none}
.bv-viewport.bv-dragging{cursor:grabbing}
.bv-world{position:absolute;left:0;top:0;transform-origin:0 0;will-change:transform}
/* Re-focusing TRAVELS to the new node instead of cutting. Only ever on while a programmatic
   centre is in flight — a transition left on permanently would put a lag on every drag. */
.bv-world.bv-gliding{transition:transform .5s var(--slide-ease)}
@media(prefers-reduced-motion:reduce){.bv-world.bv-gliding{transition:none}}
.bv-links{position:absolute;left:0;top:0;overflow:visible;pointer-events:none;
  width:1px;height:1px}
.bv-nodes{position:absolute;left:0;top:0;pointer-events:none}

.bv-node{position:absolute;transform:translate(-50%,-50%);border-radius:50%;
  border:1px solid rgba(255,255,255,.22);padding:0;cursor:pointer;
  pointer-events:auto;display:flex;align-items:center;justify-content:center;
  color:#04110e;font:600 10px var(--ui);transition:transform .12s ease}
.bv-node:hover{transform:translate(-50%,-50%) scale(1.12);z-index:5}
.bv-node:focus-visible{outline:2px solid var(--accent);outline-offset:2px}
.bv-label{max-width:120px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;
  text-shadow:0 1px 2px rgba(0,0,0,.5);pointer-events:none;padding:0 4px}
.bv-node.bv-root{border:2px solid #ffe9b0;
  background:radial-gradient(circle at 40% 35%, #ffe6a8, #eda733 55%, #c07d18);
  box-shadow:0 0 46px rgba(237,167,51,.75),0 0 14px rgba(255,230,160,.9);
  color:#2a1c02;font-size:12px}
.bv-node.bv-admin{border-color:rgba(127,212,255,.7)}
/* TAG node (treeview kind:'tags'): a name pill, not a jewel. It keeps .bv-node's absolute
   placement and hover lift and drops everything that assumes a circle of a computed size —
   the size here is the length of the word. The k-<kind> namespace colour comes from the
   shared .tag chip rules, so it matches the columns above it exactly. */
/* ⚠ THE FILL IS LOAD-BEARING, and it must not be var(--bg) — THERE IS NO --bg IN THIS
   STYLESHEET (the palette is --void/--panel/--raised/--sunk). An undefined custom property
   makes the declaration invalid at computed-value time, i.e. the background falls back to
   TRANSPARENT — which is exactly how the branch lines ended up bleeding through the chips
   and over the tag names. Every other node kind here sits on open space; these sit ON the
   branches that connect them, so an opaque fill is what makes the name readable. */
.bv-node.bv-tnode{border-radius:3px;width:auto;height:auto;padding:3px 7px;
  box-shadow:none;background:var(--panel);white-space:nowrap;animation:none}
.bv-node.bv-tnode.bv-focus{box-shadow:0 0 0 3px color-mix(in srgb, currentColor 24%, transparent)}
.bv-node.bv-banned{background:repeating-linear-gradient(45deg,#2a2c30,#2a2c30 3px,#22242a 3px,#22242a 6px);
  border:1px dashed #555b66;color:#8b929c;box-shadow:none}
.bv-node.bv-orphan{border-style:dashed;border-color:#8a6d3b}

/* ── THE TWO CLASSES IN THE BRANCH VIEW (docs/SPEC_scion.md §3.2 + §3.6) ──────────────────
   ⚠ AN ICON — OR A CLASS — NEEDS A CSS BOX. treeview.js puts `bv-binkie` / `bv-scion` on the
   chip and `bv-link-scion` on the branch path; without these four rules the whole feature
   ships INERT and looks exactly like a working tree. That has happened six times on this
   feature; these are its boxes.

   ⚖ THE HUES COME FROM THE IDENTITY VARS, NEVER A HEX AT A CALL SITE. `--binkie` (pink) and
   `--scion` (green) are declared once in app-rank.css beside the other identity colours, and
   this is the branch view's ONLY consumption point — treeview.js contains no colour at all
   for either class, so a re-tuned hue moves the names, the marks and these branches together.
   ⚠ THE FALLBACKS ARE A TRIPWIRE, NOT A CHOICE. `--scion` USED TO MEAN CONTENT-BLOCKED and
   held a citron; if that variable ever goes missing again these render in the v2 values rather
   than in nothing, but a var() that has to fall back means the identity block was edited
   without this consumer — go and read app-rank.css §3.6, do not retune them here.

   ⚖ SCION WINS OVER BINKIE, AND THE COLLAPSE HAPPENS EXACTLY HERE. One element, one colour
   (§3.6): both classes are on the chip so each stays independently assertable, and the
   `:not(.bv-scion)` is the whole of the precedence rule — no cascade-order luck, no
   !important, and reading it does not require knowing which rule was written first.

   ⚖ ONLY THE NAME TAKES THE COLOUR. The chip body keeps its quality/trouble channels: a
   scion is "drawn like any other node — same size, same strength, same brightness channels",
   and repainting the jewel would delete a real measurement to say a categorical thing. */
.bv-node.bv-binkie:not(.bv-scion) .bv-label{color:var(--binkie,#ff5ce1)}
.bv-node.bv-scion .bv-label{color:var(--scion,#00e676)}
/* THE CLASS EDGE. Same `3 7` dash rhythm the severed stump uses (one visual vocabulary for
   "not a plain vouch"), told apart by COLOUR because it makes the opposite claim: nothing
   here is dead or broken — it is a root system that has not been pulled into the tree yet.
   `stroke` is set by treeview.js as a PRESENTATION ATTRIBUTE, which loses to any stylesheet
   rule, so this one line wins the branch without !important. */
.bv-links .bv-link-scion{stroke:var(--scion,#00e676)}
/* DORMANT (dormancy v1, owner ruling 2026-07-27c) — HOLLOW. Deliberately NOT the banned
   treatment: no hatch, no †, no severed branch, because a tombstone reads as wrongdoing and
   dormancy is only absence. The ring keeps the node's own quality/trouble colour (set inline
   by treeview.js, which also clears the fill), so a dormant member is still readable as a
   member; what it loses is presence — the solid body and the outward glow. It also stops
   BREATHING below, which is the point: the tree's liveness channel goes quiet for them. */
.bv-node.bv-dormant{border-width:2px;opacity:.8;color:#c3c9d2}
.bv-node.bv-dormant .bv-label{opacity:.75;font-style:italic}
.bv-cross{position:absolute;font-size:15px;color:#aeb4bd;line-height:1;
  text-shadow:0 1px 2px #000;pointer-events:none}

/* breathing: barely-there jewel pulse; OFF under reduced-motion. */
@keyframes bvbreathe{0%,100%{filter:brightness(1)}50%{filter:brightness(1.14)}}
.bv-node:not(.bv-banned):not(.bv-dormant){animation:bvbreathe 5.5s ease-in-out infinite}
.bv-node.bv-root{animation-duration:7s}
@media(prefers-reduced-motion:reduce){.bv-node{animation:none!important}}

.bv-tip{position:absolute;transform:translate(-50%,-100%);z-index:var(--z-tooltip);
  pointer-events:none;background:rgba(8,10,13,.96);border:1px solid var(--hair-hi);
  padding:6px 9px;min-width:120px;max-width:240px;box-shadow:0 6px 20px rgba(0,0,0,.5)}
.bv-tip-name{color:var(--bright);font:600 12px var(--ui)}
.bv-tip-stat{color:var(--dim);font-size:10.5px;margin-top:2px}
.bv-tip-flag{color:var(--bad);font-size:10px;margin-top:3px;letter-spacing:calc(.05em * var(--track))}

.bv-panel{position:absolute;right:12px;top:12px;z-index:var(--z-menu);width:250px;
  background:rgba(12,15,20,.97);border:1px solid var(--hair-hi);padding:12px;
  box-shadow:0 10px 34px rgba(0,0,0,.6)}
.bv-panel-head{display:flex;align-items:baseline;gap:4px}
.bv-panel-name{color:var(--bright);font:600 14px var(--ui)}
.bv-panel-x{margin-left:auto;background:none;border:none;color:var(--dim);
  font-size:18px;line-height:1;cursor:pointer}
.bv-panel-x:hover{color:var(--bright)}
.bv-panel-stats{margin:6px 0;font-size:10.5px}
.bv-panel-flag{color:var(--bad);font-size:10.5px;margin-bottom:6px;letter-spacing:calc(.04em * var(--track))}
.bv-panel-acts{display:flex;flex-direction:column;gap:6px;margin-top:8px}
.bv-act{border:1px solid var(--hair-hi);background:var(--raised);color:var(--accent);
  font:600 11px var(--ui);text-transform:var(--caps);letter-spacing:calc(.08em * var(--track));
  padding:7px 10px;text-align:center;cursor:pointer}
.bv-act:hover{border-color:var(--accent)}
.bv-act-danger{color:var(--bad);border-color:rgba(224,85,95,.5)}
.bv-act-danger:hover{border-color:var(--bad)}
.bv-panel-msg{color:var(--bad);min-height:12px;margin-top:6px;font-size:10.5px}

/* =====================================================================
   GENEALOGY view v1 (docs/SPEC_genealogy.md) — a post's VARIANT family drawn by
   the reused branch-view engine (treeview.js kind:'genealogy'), in a focused
   full-bleed overlay (the shred spiral behind). Nodes are thumbnail tiles: the
   FOCUS post gets an amber ring, a masked node is an ashen blank, root reads as
   the template. No drift animation, so nothing to disable for reduced-motion.
   ===================================================================== */
.famhead{display:flex;align-items:center;justify-content:space-between;gap:10px;margin-bottom:6px}
.gen-open{border:1px solid var(--hair-hi);background:var(--raised);color:var(--accent);
  padding:4px 10px;font:11px var(--ui);letter-spacing:calc(.05em * var(--track));white-space:nowrap}
.gen-open:hover{border-color:var(--accent);color:var(--bright)}
.gen-overlay{position:fixed;inset:0;z-index:var(--z-overlay);display:flex;flex-direction:column;
  background:radial-gradient(ellipse at 50% 40%,rgba(16,19,24,.86),rgba(8,9,11,.97));
  backdrop-filter:blur(2px)}
.gen-bar{display:flex;align-items:center;gap:12px;padding:12px 16px;
  border-bottom:1px solid var(--hair);flex-wrap:wrap}
.gen-title{color:var(--bright);font:600 13px var(--ui);letter-spacing:calc(.06em * var(--track))}
.gen-hint{color:var(--dim);font-size:10.5px;letter-spacing:calc(.04em * var(--track))}
.gen-bar .gen-spacer{margin-left:auto}
.gen-x{border:1px solid var(--hair-hi);background:var(--raised);color:var(--ink);
  width:30px;height:28px;line-height:1;font-size:16px}
.gen-x:hover{border-color:var(--accent);color:var(--bright)}
.gen-stage{position:relative;flex:1 1 auto;min-height:0}
/* the mount fills the stage (TreeView adds .bv-viewport = absolute inset:0). */
.gen-stage>div{position:absolute;inset:0}
/* genealogy node = a square thumbnail tile (overrides the round invite chip). */
.bv-node.bv-gnode{border-radius:4px;overflow:visible;background:var(--sunk);
  border:1px solid var(--hair-hi);padding:0;transition:transform .12s ease}
.bv-node.bv-gnode:hover{transform:translate(-50%,-50%) scale(1.14);z-index:6}
.bv-gnode .bv-thumb{width:100%;height:100%;object-fit:cover;border-radius:3px;display:block}
.bv-gnode.bv-groot{border-color:var(--navy);
  box-shadow:0 0 14px rgba(221,233,243,.28)}
.bv-gnode.bv-focus{border:2px solid var(--accent);
  box-shadow:0 0 0 3px rgba(237,167,51,.28),0 0 20px rgba(237,167,51,.55);z-index:4}
.bv-gnode.bv-masked{background:repeating-linear-gradient(45deg,#20232a,#20232a 4px,#191c22 4px,#191c22 8px);
  border-style:dashed;border-color:#3a4048}
.bv-badge{position:absolute;right:-4px;bottom:-4px;min-width:15px;height:15px;
  padding:0 3px;border-radius:3px;background:rgba(10,11,13,.9);border:1px solid var(--hair-hi);
  color:var(--bright);font-size:9px;line-height:15px;text-align:center;letter-spacing:calc(.02em * var(--track))}
@media(prefers-reduced-motion:reduce){.bv-node.bv-gnode{transition:none}}


/* ═══ THE BRANCH-VIEW KEY (rebuilt 2026-08-10) ═══════════════════════════════════════════
   Each entry draws the mark the graph actually draws. The quality ramp is the SAME five steps
   treeview.js fills nodes with (one hue, monotone lightness, validated L 0.43-0.85 in OKLCH) —
   written here as literals because CSS cannot call the JS, and pinned by branchview.test.js so
   the key can never quietly disagree with the map it explains. */
.bv-legend{display:flex;flex-wrap:wrap;gap:10px 18px;align-items:center;margin-top:2px}
.bv-key{display:inline-flex;align-items:center;gap:7px}
.bv-key-t{font-size:10.5px;letter-spacing:calc(.04em * var(--track));color:var(--dim);white-space:nowrap}
.bv-ramp{display:inline-flex;gap:2px}
.bv-sw{display:inline-block;width:12px;height:12px;border-radius:50%;flex:0 0 auto}
.bv-q0{background:hsl(212,36%,30%)} .bv-q1{background:hsl(212,44%,39%)}
.bv-q2{background:hsl(212,52%,48%)} .bv-q3{background:hsl(212,58%,57%)}
.bv-q4{background:hsl(212,62%,66%)}
/* HUE = CLASS. The key shows the class at its mid step, which is what most orbs will be. */
.bv-sw-scion{background:hsl(156,66%,52%)}
.bv-sw-binkie{background:hsl(312,66%,52%)}
/* strength is a LINE, so its swatch is a line — two weights, the range the graph draws */
.bv-sw-strength{width:22px;height:12px;border-radius:0;background:
  linear-gradient(to bottom,transparent 3px,hsl(212,52%,48%) 3px,hsl(212,52%,48%) 4px,
  transparent 4px,transparent 7px,hsl(212,52%,48%) 7px,hsl(212,52%,48%) 10px,transparent 10px)}
.bv-sw-trouble{background:hsl(212,52%,48%);box-shadow:inset 0 0 0 3px hsla(356,70%,58%,.9),inset 0 0 0 4px var(--void)}
.bv-sw-dormant{background:transparent;border:1px solid hsl(212,52%,48%)}
.bv-sw-orphan{background:transparent;border:1px dashed #8a6d3b}
.bv-sw-banned{background:repeating-linear-gradient(45deg,#2a2c30,#2a2c30 3px,#22242a 3px,#22242a 6px);
  border:1px dashed #555b66}
/* ⚰ `.bv-trouble{z-index:3}` IS GONE. It existed because the trouble ring used to paint
   OUTSIDE the node and collide with neighbours; the ring is inset now, so there is nothing to
   lift above anything. A z-index that exists to hide a geometry problem outlives the problem. */
