/* app-rank.css — POINTS, THE ASCENT rank tiers and the AWARDS v2 six-band ladder.

   ⚖ 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 852-1545). 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. */
/* =====================================================================
   POINTS — the account rail + the juice. ⚑ THE RAIL IS A COLUMN, NOT A
   FLOATING CARD (owner ruling 2026-09-01, measured at 1998x1186: the
   dossier hung mid-air on the right with 290px of empty viewport above
   it). At RAIL_BP the .rail-slot track is a VISIBLE STRIP flush with the
   viewport's right edge, running from the bottom of the masthead to the
   bottom of the viewport; the dossier sits at the TOP of it and the space
   under it is deliberately empty for now. Below the breakpoint the whole
   strip is media-queried away and the header score chip takes over.
   ⚖ AND IT PUBLISHES ITS FOOTPRINT AS `--rail-w`, which is the other half
   of the ruling: a surface that spans the window (web/clubs.css) reserves
   the strip with `padding-right: var(--rail-w, 0px)` instead of guessing.
   The token is declared ONLY inside the RAIL_BP block, so `var(--rail-w,
   0px)` is 0 at every width where there is no strip. Pages still on the
   centered 1200px column (.wrap's margin:0 auto) are unmoved: the strip
   stands in blank space they were never using. All motion honors
   prefers-reduced-motion (instant number, no dot/floater/flash).

   RAIL_BP breakpoint math (content is .wrap, max-width:1200):
     free space per side  = (VW - 1200) / 2
     strip footprint      = PAD(16) + RAIL_W(260) + PAD(16) = 292 = --rail-w
     need (VW-1200)/2 >= 292  ->  VW >= 1784  ->  breakpoint set at 1800px
     (a clean round number just past the exact threshold, so the strip's
     left edge always clears the content's right edge with >=16px to spare)
   ===================================================================== */
.rail{display:none}                    /* off by default; shown on wide only */
.rail-slot{display:none}
/* ⚖ 44px OF CHIP, 23px OF DRAWN CHIP (forage-cgy) — docs/DECISIONS.md "The masthead's hit area is 44px and its drawn chrome is not". Both chips were 23px on 19 surfaces, roughly half the floor, on the bar every screen carries; and they are deliberately QUIET 11px furniture, so the answer is not louder type. The BOX takes --tap-min, the hairline moves to a ::after band of the height it always had, and an equal negative margin-block hands the growth back to the row — the drawn chip, the bar's height and --rail-clear are all unmoved at every width. .navburger (app.css) and .cas-door (casino.css) carry the same three lines. ⚠ The border STAYS on the element as `transparent`: it is 1px of the chip's width on each side, and dropping it would move the text. ⚠ AND THE BAND NEEDS `box-sizing:border-box` (forage-s2g, 2026-08-20): app.css's `*` default DOES NOT MATCH PSEUDO-ELEMENTS, so `height:23px` + a 1px border drew **25px** and "the drawn chip is unmoved" was false by 2px on every surface carrying the bar. */
.scorechip{--band:23px;display:inline-flex;align-items:center;gap:5px;margin-left:6px;position:relative;min-height:var(--tap-min);min-width:var(--tap-min);margin-block:calc((var(--band) - var(--tap-min)) / 2);
  padding:2px 8px;border:1px solid transparent;color:var(--accent);
  letter-spacing:calc(.08em * var(--track));font-size:11px;vertical-align:middle;text-decoration:none;cursor:pointer}
.scorechip::after,.coinchip::after{content:"";position:absolute;left:-1px;right:-1px;top:50%;height:var(--band);transform:translateY(-50%);box-sizing:border-box;border:1px solid var(--hair-hi)}
.scorechip:hover::after{border-color:var(--accent)}
/* ═══ ONE COLUMN OF NUMBERS, AND THE RUN BESIDE IT (owner sketch 2026-08-26) ════════════════
   Owner: *"we stack score and coins vertically and add the arcade info in the middle while
   applicable"*, and then, correcting a first build that made them two separate columns:
   *"no, coins and score sit on top of each other, symbol to the left."*
   So `.numstack` is ONE object holding both links — COINS on top, SCORE under it — and each
   line keeps the shape it always had, `mark · number` reading left to right. The bar is
   `brand · [arcade column] · [numstack] · menu`.
   ⚖ WHY A STACK IS NARROWER THAN A ROW, which is the whole reason a third thing fits on a
   390px bar at all: side by side the two chips cost the SUM of their widths plus the gap
   between them; stacked they cost the WIDER OF THE TWO. Measured on the real bar at 390 with
   19,455 coins that is 81 + 8 + 63 = 152px against 82px. ⚠ An edit that puts these two back
   on one line has spent the room the arcade column is standing in.
   ⚠ THE 44px TAP FLOOR CANNOT SURVIVE TWO STACKED LINKS AND THIS IS THE HONEST ACCOUNTING.
   docs/DECISIONS.md "The masthead's hit area is 44px and its drawn chrome is not" (forage-cgy)
   gave each chip a --tap-min BOX with the drawn pill as a ::after band and a negative
   margin-block handing the growth back to the row. That trick works for ONE link in a row: the
   oversized box overlaps the bar's own padding, which is dead space. Two links stacked would
   overlap EACH OTHER by 20-odd px, and in an overlap the later element takes the tap — so
   half of the purse's target would silently belong to the score. What each row takes instead
   is every pixel it can have WITHOUT overlapping its neighbour: the top row grows UPWARD into
   the masthead's top padding, the bottom row DOWNWARD into its bottom padding, each handing
   the growth back with a negative margin exactly as forage-cgy does. Measured at 390 that is
   23px for coins and 26px for score, against a 44px floor — the real cost of the merge, and it
   is a layout consequence rather than a choice about type. ⛔ Do not "fix" it by giving either
   row `min-height:var(--tap-min)`: that is the overlap, and it makes one target a lie.
   ⚠ SCOPED TO `.masthead` BECAUSE `.coinchip` ALSO RIDES THE CASINO BAR (`header.cas-mast`,
   casino.css), where it is the ONE archive control and has no score to stack with. The floor's
   purse is untouched; the base rules above are still the ones it wears. */
/* ⚠ NO VERTICAL PADDING, AND THAT IS 4px OF THE BAR'S HEIGHT. Two lines are taller than one no
   matter what, so every pixel here is one the whole site carries above the fold on every screen:
   measured at 390, the stack with 2px of padding put the masthead at 58.3 against today's 53, and
   without it the bar comes back to 54.3. The rows already own their own breathing room — they
   grow into the bar's padding for the tap area (below) — so this was space paid for twice.
   ⚖ AND NO BORDER EITHER, AS OF 2026-08-26 (owner, on the shipped bar: *"can we hide the box
   around everything up top aside from MENU"*). A census of every painted enclosure under
   `.masthead` — border, ::after band, box-shadow ring and outline, on every node and both
   pseudos, at 390 and 1280 — found exactly one box around a readout, and it was this one. What
   is deliberately still drawn: `.navburger::after` (the MENU box he kept), the bar's own bottom
   rule, the volume slider's track, the drawer panel's edge and its form fields, and the arcade
   badge's HEAT GLOW, which is a blurred shadow with no border and is a readout, not a box.
   ⚠ THE SIDE PADDING STAYS AND IS NOW LOAD-BEARING. It was breathing room inside a box; with
   the box gone it is the only thing holding the coins number off the arcade column beside it —
   7px here plus the row's own 8px gap. ⛔ Do not "clean it up" as leftover box padding. */
.numstack{display:flex;flex-direction:column;align-items:stretch;gap:0;position:relative;
  padding:0 7px}
/* ⚠ ONE BOX AROUND THE PAIR, NOT TWO. Each chip drew its own 23px hairline band when they were
   two separate objects on the row; stacked, two bands read as a ladder and say the opposite of
   what the sketch says, which is that this is one column. The band moves to the stack. */
.masthead .scorechip,.masthead .coinchip{--band:0px;min-height:0;min-width:0;margin:0;
  padding:0;border:none;letter-spacing:0;gap:5px}
.masthead .scorechip::after,.masthead .coinchip::after{content:none}
/* ⚠ THE MARKS KEEP THEIR OWN SIZES AND SHARE A 16px SLOT. They are deliberately different (16
   vs 11, measured ink — the note below) and neither may be resized: `.scoremark`'s 16px is the
   box `.pointdot` flies in, `.coinmark`'s 11px is what made the two read as equals. Margin, not
   width, is what lines the two NUMBERS up under each other. */
.numstack .coinchip .coinmark{margin-inline:2.5px;vertical-align:0}
/* COINS ON TOP, QUIET; SCORE UNDER IT, BIG — the sketch's own hierarchy (`20,000` small over
   `3,780` large). 11px is the furniture size this bar has always used. */
.numstack .coinchip{font-size:11px}
.numstack .coinchip-n{font-size:11px;line-height:1.2}
.numstack .scorechip-n{font-size:14px;line-height:1.15}
/* the tap area, taken out of the bar's padding rather than out of the other link (see above) */
.numstack .coinchip{padding-top:8px;margin-top:-8px}
.numstack .scorechip{padding-bottom:8px;margin-bottom:-8px}
/* ⚠ HOVER AND FOCUS WERE BOTH RIDING CHROME THAT NO LONGER EXISTS, AND ONE OF THEM WAS ALREADY
   BROKEN BEFORE HE ASKED. Hover was `border-color` on the box (and, before that, on each chip's
   ::after band). Focus is the serious half: app.css turns the site's own `:focus-visible` ring
   OFF for these two links and re-draws it on `.scorechip::after` / `.coinchip::after` — and the
   masthead set those bands to `content:none` when the chips became a stack, so from 81b9eaa
   until now THERE HAS BEEN NO VISIBLE KEYBOARD FOCUS on the purse or the score in this bar.
   Measured, not deduced: `outlineStyle:none` on both while `:focus-visible` matched.
   ⚖ SO THE RING COMES BACK ONTO THE ELEMENT, which is what the site does everywhere else
   (app.css `:focus-visible`), and hover becomes an UNDERLINE ON THE NUMBER — the idiom
   `.rail-score` already uses for exactly this thing: the same score, drawn bare with no box, on
   the wide-screen rail. The mark is left out of the underline: a rule under an SVG reads as a
   glitch, and the number is the word here. */
.numstack .scorechip:hover .scorechip-n,.numstack .coinchip:hover .coinchip-n{
  text-decoration:underline;text-decoration-thickness:1px;text-underline-offset:2px}
.masthead .scorechip:focus-visible,.masthead .coinchip:focus-visible{
  outline:1px solid var(--accent);outline-offset:1px}

/* ═══ THE ARCADE COLUMN — the live run, in the middle of the bar ══════════════════════════════
   Three lines, top to bottom: the MULTIPLIER, the run SCORE, and the countdown. The reasoning,
   and the floating pill this replaces, are at mountArcadeHud() in web/common-points.js.
   ⚠ ABSENT, NOT EMPTY, BETWEEN RUNS — `display:none` until arcadeHudSync() adds `.on`. "An idle
   run is not a readout, it is litter", and at rest the bar is a clean two-column bar with no gap
   held open for a number that is not there.
   ⚠ NO HIT AREA, ON PURPOSE. Its neighbour is a pair of LINKS fighting for a tap floor
   (`.numstack` above); this is `pointer-events:none` — there is nothing to tap — so it draws
   its lines and takes no box beyond them. It is also deliberately no TALLER than the stack
   beside it, which is what keeps the bar exactly as tall during a run as it is at rest.
   ⚠ `position:relative` IS LOAD-BEARING AND IS THE THIRD LINE. `.mb-fill` — the run's 10-minute
   chain countdown — is absolutely positioned at `bottom:0;left:0` inside `.multbadge`, and the
   flat badge below is deliberately `position:static` so the fill measures itself against THIS
   box instead: a 2px rule draining right-to-left along the foot of the whole column. The meter
   has no minutes-remaining NUMBER to print (armCountdown drives a width transition, not a
   clock), so the countdown is drawn as the countdown rather than invented as a numeral. */
/* ⚠ 30px IS "NO TALLER THAN THE STACK BESIDE IT", MEASURED. Its three lines come to 28px of
   ink; the height is fixed rather than shrink-wrapped so the countdown rule has a foot to sit on
   at every multiplier width — and fixed AT the neighbour's height so that starting a run cannot
   grow the masthead. If `.numstack` ever changes size, this number is the one that follows it. */
.arcadecol{display:none;flex-direction:column;align-items:center;justify-content:center;
  position:relative;height:30px;padding:1px 5px;pointer-events:none}
.arcadecol.on{display:inline-flex}
/* ⚠ SCORE AND COINS MUST NOT LOOK ALIKE (owner ask 2026-08-05: *"coins and points need more distinct icons"*). THREE axes, because any one alone fails at 10px on a phone: SHAPE (a sparkle vs a ring),
   COLOUR (accent amber vs the paler coin gold), SKIN (this rides currentColor and repaints — maroon under yotsuba; the coin is hardcoded gold in every skin, because money is gold). ⚖ TWO MORE WERE
   TRIED AND ARE SPENT. RENDER (typed glyph vs drawn object) died when score was drawn too (2026-08-09). RELIEF (flat vs lit dome) lasted ONE AFTERNOON: the coin was drawn as a shaded sphere
   2026-08-19 and overruled at 5x the same day — *"we can't have icons with gradients and multiple colors then another as a single color."* ⛔ EVERY MARK HERE IS FLAT, ONE COLOUR. Hue and silhouette.
   ⚠ AND SCORE'S BOX IS 16px, NOT 13 — it moves WITH `.pointdot` below or every flying score lands half a box off the chip. The mark is the TWINKLE PAIR as of 2026-08-19 (owner pick, "for now") and a
   star is mostly air: measured as summed alpha at true rendered size, the pair carries 48.1 ink at 16px against the flat coin's 55.9 at 11 — the same 86-89% of the purse the stripe held at 13. At
   13px the pair is 30.7 and its small spark rounds off to a blob. Full ink ladder and the geometry: the SCORE MARK comment in common-icons.js. Changing the shape means re-measuring both numbers. */
.scoremark{width:16px;height:16px;display:block;flex:0 0 auto;fill:currentColor;color:var(--accent-deep)}
/* ⚖ AND THE COIN'S BOX SITS BESIDE IT: as of 2026-08-19 the two marks are ONE STANDARD (owner ask
   *"the coin and score icons are different styles"* — the purse was a CSS gradient disc here and a
   typed U+25C9 on five other surfaces). Both are svgs from common-icons.js, boxed here, carrying no
   size of their own. PAINT is the one thing they do NOT share: the sparkle rides --accent via
   currentColor, the coin's single gold literal is in its geometry where no skin reaches it — and
   NOTHING HERE MAY OVERRIDE IT; a fill: on .coinmark hands the skin a way in. 11px vs 13 is optical:
   a round mark fills its box where two bars are mostly air (measured). ⚠ inline-block+vertical-align
   so ONE rule serves the flex chips AND the casino's display numbers, re-boxed in `em` to track it.
   ⚠ /wallet is NOT in that list any more: `.pns-wallet-coin` (app-screens.css) sizes its instance. */
.coinmark{width:11px;height:11px;display:inline-block;vertical-align:-1px;flex:0 0 auto}
.cas-balance .coinmark,.tnt-potn .coinmark{width:.66em;height:.66em;vertical-align:-.04em;margin-right:.04em}
/* ⚖ THE COINS NUMBER IS GOLD, THE SCORE NUMBER IS NOT (owner 2026-08-26, on the stacked bar:
   *"perhaps we also color the coins numbers as the same gold?"*). ⚠ THIS STRENGTHENS "SCORE AND
   COINS MUST NOT LOOK ALIKE" RATHER THAN DECORATING IT. That ruling wanted three axes — shape,
   colour, skin — and stacking the two links into one column had quietly spent two of them: the
   rows sat one above the other in the same white, differing only by the mark at the far left.
   Colouring the number moves the difference into the widest, brightest thing in each row, which
   is the part a thumb actually reads. The score keeps `--bright`: it is the identity number, and
   two golds would put the axes back where they were.
   ⚠ `--coin-ink`, NOT `--gold` DIRECTLY (app.css). Money is gold on the casino's own dark chrome
   at 8.75:1, but this number is text on the PAGE, and on the two light skins the same gold
   measures 2.23:1 (yotsuba) and 2.01:1 (yotsuba-b) — below the 4.5:1 floor and worse than the
   two failures fixed on 2026-08-26. Those skins move the ink and nothing else; every dark skin
   uses the money colour unchanged. ⛔ Do not collapse the two tokens back together. */
.scorechip-n{color:var(--bright);font-variant-numeric:tabular-nums;display:inline-block}
.coinchip-n{color:var(--coin-ink);font-variant-numeric:tabular-nums;display:inline-block}
/* ⚠ THE FLOOR'S OWN BAR IS SKIN-PROOF, SO IT KEEPS THE MONEY COLOUR. `.cas-mast` (casino.css)
   paints a hardcoded `linear-gradient(180deg,#15120c,#0e0c08)` whatever the skin says, and the
   SAME purse element rides it (casSettle updates it by id). A light skin's darkened ink would be
   dark gold on near-black there — the floor broken in the other direction. */
.cas-mast .coinchip-n{color:var(--gold)}

/* ---- the dossier (RAIL_W=260). .rail-slot is a fixed, VISIBLE vertical
   STRIP, --rail-w (292px) wide and flush with the viewport's right edge
   (right:0), running from the bottom of the masthead down to the bottom
   of the viewport. Its top is `--rail-clear - 16px` because --rail-clear
   is the masthead's real rendered height PLUS a 16px gap (common.js
   renderHeader(), measured off the live bar so no number here can drift)
   and the strip's own 16px padding is that gap — so the strip's edge sits
   flush under the masthead and its CONTENT still starts 16px below it.
   ⚑ align-items:safe center IS GONE (owner ruling 2026-09-01): the card
   sits at the TOP of the strip (flex-direction:column + align-items:
   stretch + flex:0 1 auto) and the room under it is deliberately empty.
   The internal scroll stays, so a dossier taller than the strip scrolls
   INSIDE it (max-height:100%, overflow-y:auto on .rail) rather than
   climbing over the masthead. The plate is the same translucent
   rgba(0,0,0,.55) `.wrap.pagecol::before` (app.css:396) and
   `.sc-life::before` (clubs.css) paint — the background logo stays
   visible through it. See RAIL_BP math above. */
@media(min-width:1800px){.scorechip{display:none}
  /* the strip's footprint, published for the surfaces that reserve it (web/clubs.css).
     ⛔ DECLARED ONLY HERE, INSIDE THE BREAKPOINT: every reader spells `var(--rail-w, 0px)`,
     so below RAIL_BP the reservation is exactly zero and the phone pays nothing. */
  :root{--rail-w:292px}
  .rail-slot{display:flex;flex-direction:column;align-items:stretch;
    position:fixed;right:0;bottom:0;top:calc(var(--rail-clear, 66px) - 16px);
    width:var(--rail-w);padding:16px;pointer-events:auto;z-index:var(--z-chrome);
    border-left:1px solid var(--hair-hi);background:rgba(0,0,0,.55)}
  .rail{display:block;width:100%;max-height:100%;flex:0 1 auto;
    overflow-y:auto;overscroll-behavior:contain;
    border:1px solid var(--hair-hi);background:var(--panel);padding:16px 15px 14px;
    box-shadow:0 0 0 1px var(--void),0 8px 30px rgba(0,0,0,.5)}
  body.is-profile .rail-slot{display:none} /* the /profile page IS the dossier; no double */
}
/* the dossier as a full page (narrow home of the rail) */
.dossier-page{max-width:340px;margin:6vh auto 0;border:1px solid var(--hair-hi);
  background:var(--panel);padding:20px 18px 16px;
  box-shadow:0 0 0 1px var(--void),0 8px 30px rgba(0,0,0,.5)}

/* avatar — a live post's thumb, center-cropped square; fallback = shred mark */
.dsr-avatar{width:96px;height:96px;border:1px solid var(--hair-hi);background:var(--sunk);
  margin:2px auto 12px;display:flex;align-items:center;justify-content:center;overflow:hidden;position:relative}
.dsr-avimg{width:100%;height:100%;object-fit:cover}
.dsr-avfallback{width:58%;height:58%;object-fit:contain;filter:invert(1);opacity:.16}
.dsr-id{text-align:center;margin-bottom:4px}
.rail-user{font-size:12px;letter-spacing:calc(.1em * var(--track));color:var(--bright);font-weight:700;
  overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
/* rail v2: the rank unit's tier NAME carries the rank-up type-in cursor (was .rail-rank) */
.rail .sig-name.typing::after{content:"_";color:var(--accent);animation:railcursor .7s steps(1) infinite}
@keyframes railcursor{50%{opacity:0}}

/* ================= THE ASCENT — rank tier colors + escalating glow =================
   .rk-<tier> paints a rank NAME or a username in its tier color + effect. Applied on
   the dossier rail-rank, the score chip, the profile sigil, the leaderboard, and
   EVERY username (userLink) — a user's rank follows them across the whole site.
   ⚠ EACH TIER IS ALSO WRITTEN `.ulink.rk-<tier>` (0,2,0) AND THAT HALF IS LOAD-BEARING, not a
   typo or a tidy-up: `.ulink{color:var(--accent)}` in app-profile.css loads LATER and beats a
   one-class ladder rule, so a username would paint flat accent. Full note there (forage-fk5). */
/* ⚖ LURKER READS `--faint`, NOT ITS OWN HEX (forage-os0n, 2026-08-25). The tier shipped
   `#5c646f` — the EXACT value the owner already retired from `--faint` on 2026-08-19 for being
   2.92:1 ("The quiet still has to be readable", docs/archive/DECISIONS.md) — and the rank pip
   beside a lurker's name measured 3.29:1 on `--void` against this repo's 4.5:1 floor. The token
   already carries the smallest lightness-only move that clears it, so the bottom tier borrows the
   floor value instead of keeping a second copy of a colour that lost. Terminal: 5.12:1 on
   `--void`. ⚠ THE TIER STILL READS AS THE QUIETEST — `--faint` sits below `--dim` and well below
   `.rk-slave`'s `#8892a2` (6.26:1), so the ladder's order survives the raise. Light skins keep
   their own `.rk-lurker` correction in app-skins.css and are untouched. */
.rk-lurker,.ulink.rk-lurker    {color:var(--faint)}
.rk-slave,.ulink.rk-slave      {color:#8892a2}
.rk-denizen,.ulink.rk-denizen  {color:#c8cdd4}
.rk-citizen,.ulink.rk-citizen  {color:#56b6d8; text-shadow:0 0 9px rgba(86,182,216,.4)}
.rk-freeman,.ulink.rk-freeman  {color:#5fc46b; text-shadow:0 0 9px rgba(95,196,107,.45)}
.rk-noble,.ulink.rk-noble      {color:#eda733; animation:rk-pulse 3.2s ease-in-out infinite}
.rk-royalty,.ulink.rk-royalty  {color:#c08cf0; animation:rk-pulse 3.2s ease-in-out infinite}
.rk-demonic,.ulink.rk-demonic  {color:#ff3b3b; text-shadow:0 0 11px rgba(255,59,59,.65); animation:rk-flicker 3s steps(1) infinite}
.rk-divine,.ulink.rk-divine    {color:#ffe6a3; text-shadow:0 0 16px rgba(255,224,150,.65),0 0 34px rgba(255,224,150,.28); animation:rk-breathe 3.6s ease-in-out infinite}
.rk-celestial,.ulink.rk-celestial{
  background:linear-gradient(90deg,#7fefff,#b79bff,#ffd98a,#8affd1,#7fefff);
  background-attachment:scroll;background-size:var(--prism-span);background-position:var(--sheet-pos,0 0);
  -webkit-background-clip:text; background-clip:text; color:transparent;
  filter:drop-shadow(0 0 10px rgba(150,200,255,.5))}
/* username links keep their tier color on hover (don't fall back to a{--bright}) */
.ulink.rk-lurker:hover{color:#7b8492}.ulink.rk-slave:hover{color:#a7b0be}.ulink.rk-denizen:hover{color:var(--bright)}
.ulink.rk-citizen:hover{color:#7fd0ec}.ulink.rk-freeman:hover{color:#84e08e}.ulink.rk-noble:hover{color:#ffc24d}
.ulink.rk-royalty:hover{color:#d3aef5}.ulink.rk-demonic:hover{color:#ff6b6b}.ulink.rk-divine:hover{color:#fff3cf}
/* ⚖ A CLIPPED SPECTRUM NEEDS A `:hover{color:transparent}` TWIN — ALL OF THEM, NOT THE ONE THAT WAS NOTICED (forage-5y0, 2026-08-21; the class forage-fk5 opened and closed once). `background-clip:text` + `color:transparent` paints the gradient THROUGH the glyphs, and any opaque `color` on the same element hides it — `app-profile.css:68` declares `.ulink:hover{color:var(--bright)}` at (0,2,0) in a LATER part, so it wins the tie on source order the instant the pointer arrives and the name goes FLAT while you point at it. ⚑ fk5 KNEW THIS AND APPLIED IT ONCE: `.ulink.rk-celestial:hover` got the twin; `.prism-text` and `.apex-name` — added later, same file, same three declarations — did not. Desktop only, so no phone-width harness could ever have found it, on the reigning #1 apex name SITE-WIDE (userLink adds `.apex-name` everywhere) and every crown holder on /leaderboard. ⚖ SO THIS IS ONE RULE OVER THE WHOLE CLASS, and the list is the point: a fourth clipped class must join this line or it breaks the same way. `server/tests/clipfillhover.test.js` derives that list from web/ and fails on any `.ulink` class that paints with `background-clip:text` and has no hover twin, so the list here can never be quietly one short. ⛔ NOT `!important` and NOT a re-order: the twin is the idiom this file already uses. */
.ulink.rk-celestial:hover,.ulink.prism-text:hover,.ulink.apex-name:hover{color:transparent}

@keyframes rk-pulse{0%,100%{filter:drop-shadow(0 0 2px currentColor)}50%{filter:drop-shadow(0 0 8px currentColor)}}
@keyframes rk-flicker{0%,100%{opacity:1}41%{opacity:1}43%{opacity:.5}45%{opacity:1}71%{opacity:.82}72%{opacity:1}}
@keyframes rk-breathe{0%,100%{text-shadow:0 0 12px rgba(255,224,150,.5),0 0 26px rgba(255,224,150,.2)}50%{text-shadow:0 0 20px rgba(255,224,150,.8),0 0 46px rgba(255,224,150,.4)}}

/* rank-up CELEBRATION: a centered flare of the new rank name in its tier effect. */
.rankup-flare{position:fixed;left:50%;top:36%;transform:translate(-50%,-50%);z-index:var(--z-juice);
  text-align:center;pointer-events:none;animation:rankup-flare 1.9s ease-out forwards}
.rankup-flare .ruf-k{font-size:11px;letter-spacing:calc(.42em * var(--track));text-transform:var(--caps);color:var(--dim)}
.rankup-flare .ruf-name{display:block;margin-top:8px;font-weight:700;letter-spacing:calc(.14em * var(--track));text-transform:var(--caps);
  font-size:clamp(30px,9vw,58px);line-height:1}
@keyframes rankup-flare{0%{opacity:0;transform:translate(-50%,-50%) scale(.6)}
  14%{opacity:1;transform:translate(-50%,-50%) scale(1.06)}
  72%{opacity:1;transform:translate(-50%,-50%) scale(1)}100%{opacity:0;transform:translate(-50%,-50%) scale(1.12)}}
/* rank-up sparks from the rail — round ON PURPOSE (no longer .pointdot's look): PARTICLES, not twelve scores arriving */
.burstdot{position:fixed;left:0;top:0;width:7px;height:7px;border-radius:50%;background:currentColor;
  box-shadow:0 0 8px currentColor;z-index:var(--z-juice-trail);pointer-events:none;will-change:transform,opacity}

/* ═══ THE JUICE, WHILE THE VIEWER IS UP (owner report 2026-08-06) ═════════════════════════
   *"the mobile tag editor should show the dopamine system, points sounds and such like normal"*
   — and it was FIRING the whole time. triage.js's tag panel calls applyPoints() on both the add
   and the vote, exactly as the desktop rows do; the sound played, the score ticked, the dots
   flew. They flew UNDERNEATH: `.pointfloat`/`.pointdot` sit at --z-juice (700) and the mobile
   viewer is --z-viewer (1200), so on a phone every point of feedback was painted behind the
   thing the user was looking at. Nothing errored, and on desktop — where no overlay exists —
   the identical code was visibly correct.

   ⚖ RE-RAISED RATHER THAN MOVED, and scoped to `body.vw-lock` (a class only the open viewer
   sets) — the same shape the modal backdrop already uses four rules down in app-viewer.css. The
   base layer is where it belongs for every non-overlay surface; this says "and when a
   full-screen overlay owns the screen, the celebration comes with it".
   ⚠ IT MUST OUT-RANK THE SHEET (--z-sheet 1400), not just the viewer: voting a comment inside
   the drag-up sheet spawns its floater over a surface that is itself above the viewer. */
body.vw-lock .pointfloat,body.vw-lock .pointdot,
body.vw-lock .rankup-flare,body.vw-lock .burstdot{z-index:var(--z-juice-top)}

/* profile SIGIL: a tier-ordinal medallion + the big glowing rank name. */
.dsr-sigil,.prof-crest{display:flex;flex-direction:column;align-items:center;gap:8px;margin:2px 0 12px}
.sig-badge{width:64px;height:64px;border-radius:50%;border:2px solid currentColor;
  display:flex;align-items:center;justify-content:center;font-size:24px;font-weight:700;
  font-variant-numeric:tabular-nums;filter:drop-shadow(0 0 8px currentColor)}
.sig-name{font-size:18px;letter-spacing:calc(.18em * var(--track));text-transform:var(--caps);font-weight:700}
/* rail v2: the sigil IS the rank unit now (was hidden in the rail) — a compact
   ONE-LINE tier badge + tier-colored rank name. The big /profile crest is unchanged. */
.rail .dsr-sigil{flex-direction:row;justify-content:center;align-items:center;gap:8px;margin:2px 0 10px}
.rail .dsr-sigil .sig-badge{width:30px;height:30px;font-size:13px;border-width:1px;flex:0 0 auto}
.rail .dsr-sigil .sig-name{font-size:12px;letter-spacing:calc(.14em * var(--track))}
/* header rank crest — anchored top-right, deliberate (not a floating circle) */
.prof-crest{flex:0 0 auto;margin:0 0 0 auto}

/* leaderboard — the ascent */
.lb{display:flex;flex-direction:column;gap:1px;margin-top:10px}
/* ⛔ `.lb-row`, `.lb-row.top`, `.lb-place` and `.lb-score` ARE GONE. f691a777 put the ladder on
   the panel primitive: page-leaderboard.js draws `.pn-row .pns-lb-row` and `.pns-lb-place` /
   `.pns-lb-score` (app-screens.css). `.lb-name` and `.lb-rank` below are STILL EMITTED — the
   repaint kept both classes beside their `.pns-lb-*` twins — so they stay here. */
.lb-name{font-size:13px;letter-spacing:calc(.02em * var(--track));overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.lb-rank{font-size:9.5px;letter-spacing:calc(.14em * var(--track));text-transform:var(--caps);text-align:right}
@media(max-width:520px){.lb-rank{display:none}}
/* 🔥 daily-streak chip accent */
.dsr-rep-chip.streak{border-color:rgba(240,130,50,.45)} .dsr-rep-chip.streak .dsr-rep-v{color:#ff8f42}
/* rename-yourself control on /profile. ⚖ THE BORDERED `.prof-rename` CHIP IS GONE (owner ask
   2026-08-08, change 5): it sat in the name row at nearly the username's weight for something
   touched twice a year. The trigger is now `.plaque-link` (app-profile.css) in the quiet
   maintenance row; the FORM below is unchanged and still owns the editing state. */
.prof-rename-form{display:flex;align-items:center;gap:8px;flex-wrap:wrap}
.prof-rename-input{font-size:17px;padding:6px 9px;background:var(--sunk);border:1px solid var(--hair-hi);
  color:var(--bright);max-width:220px;letter-spacing:calc(.02em * var(--track))}
.prof-rename-msg{color:var(--dim)}

@media(prefers-reduced-motion:reduce){.rk-noble,.rk-royalty,.rk-demonic,.rk-divine,.rk-celestial{animation:none}
  /* .rk-celestial needs no background-position park any more — the iris animation is gone and
     the spectrum is a fixed sheet, so it is already still (2026-07-27). */
  .rankup-flare{animation:rankup-flare-rm 2s ease-out forwards}
  @keyframes rankup-flare-rm{0%,80%{opacity:1}100%{opacity:0}}
}

/* section idiom: a bordered lowercase header over each block */
.dsr-sec{margin-top:14px}
.dsr-h{font-size:9px;letter-spacing:calc(.2em * var(--track));text-transform:var(--caps);color:var(--faint);
  padding-bottom:5px;margin-bottom:9px;border-bottom:1px solid var(--hair)}
.dsr-sec .dsr-h::before{content:"◈ ";color:var(--accent-deep)}

.rail-score{display:block;font-size:38px;line-height:1.05;color:var(--bright);font-variant-numeric:tabular-nums;
  margin:2px 0 10px;transform-origin:left center;will-change:transform;text-decoration:none;cursor:pointer}
.rail-score:hover{text-decoration:underline}
.rail-track{height:3px;background:var(--sunk);border:1px solid var(--hair);overflow:hidden}
.rail-fill{height:100%;background:var(--accent);width:0;transition:width .3s ease-out}
.rail-next{font-size:10px;letter-spacing:calc(.06em * var(--track));color:var(--dim);margin-top:7px}

/* record — one stat per line, dim label / tabular value */
.dsr-stats{display:flex;flex-direction:column;gap:5px}
.dsr-stat{display:flex;justify-content:space-between;align-items:baseline;gap:10px;font-size:11px}
.dsr-k{color:var(--dim);letter-spacing:calc(.04em * var(--track))}
.dsr-v{color:var(--ink);font-variant-numeric:tabular-nums;text-align:right}
/* negative burn score (owner ruling 2026-07-21) — the SAME red the admin
   users table's warncell uses for strikes, applied to just this row's value. */
.dsr-stat.bad .dsr-v{color:var(--bad)} .dsr-stat.good .dsr-v{color:var(--good)}

/* burn dashboard (owner ruling 2026-07-21) — its own bordered box, first on
   the stats tab, so it reads as a distinct instrument rather than one more
   line among a dozen other stats. Red-accented border/wash when unhealthy. */
.burn-dash{border:1px solid var(--hair-hi);border-radius:5px;padding:10px 12px;margin-bottom:14px;background:var(--raised)}
.burn-dash.burn-bad{border-color:var(--bad);background:color-mix(in srgb, var(--bad) 10%, var(--raised))}
.burn-dash .prof-lbl{margin-bottom:6px}

/* CREATOR VERIFICATION v1 (owner ruling 2026-07-21i) — an always-visible box on
   the post page, tinted with CONTENT's fire palette (the bonus lands in the
   content bar). Same bordered-box idiom as .burn-dash. */
.creator-claim{border:1px solid var(--hair-hi);border-radius:5px;padding:10px 12px;margin:0 0 14px}
.cc-head{margin-bottom:6px}
.cc-lbl{font-size:10px;letter-spacing:calc(.16em * var(--track));text-transform:var(--caps);color:#ff8a3d}
.creator-claim.cc-pending{border-left:3px solid #f2792b}
.creator-claim.cc-verified{border-color:var(--good);background:color-mix(in srgb, var(--good) 8%, var(--raised))}
.creator-claim.cc-rejected{border-color:var(--bad);background:color-mix(in srgb, var(--bad) 8%, var(--raised))}
.cc-body{display:flex;justify-content:space-between;align-items:baseline;gap:10px;font-size:12px;flex-wrap:wrap}
.cc-body.cc-good{color:var(--good);justify-content:flex-start} .cc-body.cc-bad{color:var(--bad);justify-content:flex-start}
.cc-net{color:var(--dim)}
.cc-votes,.cc-admin{display:flex;gap:8px;margin-top:8px;flex-wrap:wrap}
.cc-msg:empty{display:none}

/* (rail medals removed — owner ruling 2026-07-19: awards are hidden on the rail;
   medals live on the /u/ profile only) */

/* awards — collectible MEDALS by rarity (icon glows in the rarity color) */
.awards-grid{display:flex;flex-wrap:wrap;gap:12px}
.award-badge{position:relative;width:106px;padding:15px 8px 10px;text-align:center;color:var(--dim);
  border:1px solid var(--hair-hi);border-radius:5px;background:var(--panel);cursor:default}
.aw-icon{font-size:30px;line-height:1;filter:drop-shadow(0 0 6px currentColor)}
/* DRAWN medals (common-icons.js AWARD_ICON, owner ask 2026-08-06). The box keeps the glow and
   the shimmer that already worked on the emoji; only the child changes from a character to an
   <svg>, so `font-size:30px` has to become a real box. 30px is the size those glyphs were drawn
   against — see the AWARD_ICON header before changing it. */
.aw-icon.aw-drawn{display:flex;align-items:center;justify-content:center;height:30px}
.aw-icon .awi{width:30px;height:30px;display:block}
/* ⚖ THE CLASS MARKS SHARE THE MEDAL'S HOST, SO THEY NEED THE SAME BOX. scionIcon() emits
   <svg class="clsi clsi-scion"> and binkieIcon() <svg class="clsi clsi-binkie">, both with a
   viewBox and NO width/height — exactly like .awi — and an SVG in that state computes to 100%
   WIDE. Without this pair a class mark on the rail renders as a full-width smear, which is one
   of the six ways this feature has shipped inert. ⚖ THE BOX IS KEYED ON `.clsi`, NEVER ON THE
   PER-CLASS MODIFIER: `.clsi-binkie` and `.clsi-scion` exist so a stylesheet can tell the two
   apart, and if the SIZE lived on them a third mark would arrive box-less again. The paint rule
   is separate from the box rule for the same reason .awi's is: currentColor + no fill is the
   icon set's contract, and a mark that paints itself is the loudest thing on the screen. */
.aw-icon .clsi{width:30px;height:30px;display:block}
svg.clsi{fill:none;stroke:currentColor;stroke-width:1.75;stroke-linecap:round;stroke-linejoin:round}
.aw-title{margin-top:8px;font-size:11px;letter-spacing:calc(.05em * var(--track));color:var(--bright)}
.aw-pct{margin-top:4px;font-size:8.5px;letter-spacing:calc(.05em * var(--track));color:currentColor;opacity:.85;font-variant-numeric:tabular-nums}
/* MEDALS ride the SAME six-band ladder (owner ruling 2026-07-20g) — by holder-% (rarer =
   higher band). Solid borders (permanent). Colors are the shared --r-* tokens. */
.award-badge.rarity-common{color:var(--r-common);border-color:rgba(154,161,173,.4)}
.award-badge.rarity-uncommon{color:var(--r-uncommon);border-color:rgba(95,196,107,.45)}
.award-badge.rarity-rare{color:var(--r-rare);border-color:rgba(77,184,255,.5);box-shadow:0 0 14px rgba(77,184,255,.14)}
.award-badge.rarity-epic{color:var(--r-epic);border-color:rgba(217,102,232,.55);box-shadow:0 0 15px rgba(217,102,232,.16)}
.award-badge.rarity-legendary{color:var(--r-legendary);border-color:rgba(255,217,138,.55);box-shadow:0 0 18px rgba(255,217,138,.2);
  background:linear-gradient(180deg,rgba(255,217,138,.06),transparent)}
.award-badge.rarity-mythic{color:var(--r-mythic);border-color:rgba(255,77,94,.6);box-shadow:0 0 20px rgba(255,77,94,.24);
  background:linear-gradient(180deg,rgba(255,77,94,.07),transparent)}
.award-badge.rarity-legendary .aw-icon,.award-badge.rarity-mythic .aw-icon{animation:aw-shimmer 3s ease-in-out infinite}
@keyframes aw-shimmer{0%,100%{filter:drop-shadow(0 0 5px currentColor)}50%{filter:drop-shadow(0 0 15px currentColor)}}
@media(prefers-reduced-motion:reduce){.award-badge.rarity-legendary .aw-icon,.award-badge.rarity-mythic .aw-icon{animation:none}}

/* ===== AWARDS v2 SIX-BAND LADDER (owner rulings 2026-07-20e/f/g) — ONE system across
   titles AND medals: common grey -> uncommon green -> rare BLUE -> epic magenta ->
   legendary GOLD -> mythic RED -> APEX prismatic (the living #1, above all bands). The six
   --r-* hexes are LAW (see docs/SPEC_awards_v2.md). Titles DASHED (dynamic), medals SOLID
   (permanent), the apex a prismatic gradient frame + saturated glow + corner molding. */
:root{
  --r-common:#9aa1ad; --r-uncommon:#5fc46b; --r-rare:#4db8ff;
  --r-epic:#d966e8; --r-legendary:#ffd98a; --r-mythic:#ff4d5e;
  /* ⚖ THE TWO CLASS HUES — ONE SOURCE OF TRUTH EACH (docs/SPEC_scion.md v2 §3.6), declared here
     beside the other identity colours rather than as hexes at the rules that use them, because
     "a class renders in its own colour EVERYWHERE a username renders" is a promise about a
     colour, and a promise about a colour needs a name. Neither is a --rk tier and neither is a
     --cat category: these say what KIND of account this is, not how it ranks and not what its
     posts contain.
     ⚠ THE HUES ARE THE OWNER'S, NOT A MEASUREMENT'S (ruling 2026-08-08c, verbatim: "the branch
     view shows binkies as PINK users, and scions as GREEN users"). v1 measured its way to citron
     because the colour was free to be anything; it is not free any more. So the measurement's
     job changed from CHOOSING the hue to choosing the best member of the named family, and both
     of these clear the same bars v1's did — ΔE(CIELAB) > 20 from every .rk-<tier> that can land
     on a name, > 30 from --cat-nsfw, and legible on --void:
       · --binkie #ff5ce1 — nearest neighbour --cat-nsfw #ff6fae at ΔE 33.7 (a mid rose; this is
         a magenta-leaning hot pink), then royalty #c08cf0 at 37.7. A CONTENT class that could be
         mistaken for the nsfw CATEGORY chip is the one confusion that would be actively
         misleading, which is why the pink was pushed toward magenta rather than toward salmon.
       · --scion #00e676 — nearest neighbour freeman #5fc46b at ΔE 24.8. Green is genuinely a
         crowded band here (freeman, --good, --cat-humor), so this one is deliberately at the
         saturated, brighter end: it is the widest gap the word "green" still admits. Below about
         ΔE 20 it would read as "ranked freeman", which is the one sentence a class must not say.
     ⚠ AND `--scion` USED TO MEAN CONTENT-BLOCKED. Repointing it at green without renaming the
     old rules would have repainted every existing protected account green and called it
     quarantined; the citron value moved WITH its meaning to --binkie (pink) and only then was a
     new --scion declared. Audit any reference one at a time — a global replace inverts this. */
  --binkie:#ff5ce1; --binkie-lo:#a3006b;   /* the -lo pair are the light-skin corrections, below */
  --scion:#00e676;  --scion-lo:#046b3a;
  --prism:linear-gradient(90deg,#d9ffe3,#d4f3ff,#e2d9ff,#ffd9e8,#ffe9c9,#fdffd1,#ffffff,#d9ffe3);
  /* saturated spectrum — the thick GLOW halo ONLY (owner: "thick glow, more saturated rainbow") */
  --prism-sat:linear-gradient(90deg,#3dff7a,#33ccff,#8a5cff,#ff4dd2,#ff9838,#ffe83d,#3dff7a);
  /* whitened spectrum (owner 2026-07-20v: "border slightly more white") */
  --prism-lite:linear-gradient(90deg,#a6ffc4,#9fe2ff,#c9b4ff,#ffb0e6,#ffd09b,#fff5ab,#a6ffc4);
  /* ============ THE SHEET (owner ruling 2026-07-27) ============================
     ONE spectrum that EVERY prismatic surface samples, instead of each effect animating its
     own gradient. Twelve crown rows are twelve windows onto one continuous surface — a row's
     hue is set by WHERE IT SITS, and the whole page reads as a single sheet seen through them.
     It is also why the lag is gone. The old pattern animated `background-position` per
     element, which cannot be composited, so each surface repainted every frame: the crowns
     page ran 24 repainting layers plus 12 `filter:blur` coronas. The sheet never animates.
     The 2026-07-20q perf note already declared animated background-position on prismatic
     surfaces BANNED; this finally removed the last of them.

     ⚠ THE SHEET IS ANCHORED TO THE DOCUMENT, NOT THE VIEWPORT (owner ruling 2026-07-27b).
     It shipped as `background-attachment:fixed`, on the theory that "motion comes from
     SCROLLING past it, which is free". THAT WAS MEASURED AND IT WAS FALSE: a viewport-anchored
     background gives every one of these surfaces DIFFERENT PAINTED CONTENT on every scroll
     frame, so all twelve rows repaint, and each repaint re-runs its two `drop-shadow`s.
     `tools/prismperf_trace.js`, phone-emulated: **960ms of paint+raster per second of wall
     clock while scrolling, against 97 anchored to the document** — a 10x difference, and the
     single most expensive thing on the page. It is cheap while PARKED, which is why four
     rounds of idle-fps testing never found it.
     So: `background-attachment:scroll` plus `--sheet-pos`, which anchorSheet() in common.js
     sets to minus each surface's document offset ONCE PER LAYOUT (never on scroll — that is
     the entire point). All the ruling asked for survives: one continuous spectrum, neighbours
     agreeing, the page as a single surface. What was traded away is the PARALLAX — the sheet
     no longer sweeps past as you scroll, the hues are pinned to the page.
     ⚖ A NEW PRISMATIC SURFACE MUST BE ADDED TO `SHEETED` IN common.js or it silently falls
     back to `0 0` and stops agreeing with its neighbours.

     ⚠ THE SPECTRUM IS 90deg, AND IT MUST STAY 90deg. It was 100deg, and the slight tilt was
     invisible inside any one element but FATAL once the sheet became document-anchored: a
     tilted gradient CANNOT TILE SEAMLESSLY, so at every multiple of the tile height the colour
     jumped and drew a hard line straight across the page. (The owner caught one between the
     oracle at docY 815 and the weaver at 866 — the 840px tile boundary exactly.) Viewport
     anchoring hid it because the wrap slid around with the scroll and never parked on a row.
     The tilt is not lost, it MOVED: anchorSheet folds each surface's vertical position into a
     HORIZONTAL phase (SHEET_AX/SHEET_AY in common.js are cos/sin of the old 100deg), so the
     hue still advances as you go down the page — by exactly the same amount as before — while
     the gradient itself only ever varies along x, which tiles perfectly because every one of
     these spectrums begins and ends on the SAME colour. Keep that property too.
     --prism-span is deliberately WIDER than the viewport so a full spectrum cycle spans a
     phone screen and no single element lands on one flat hue. Its width is the old 100deg
     gradient LINE LENGTH (140vw·cos + 100vh·sin), so hue density is unchanged by the flip. */
  --prism-span:calc(137.873vw + 17.365vh) 100vh}
/* prism TEXT — clipped cycling spectrum + COLORED cycling glow (owner ruling 2026-07-20i:
   the glow itself cycles the spectrum in sync with the slide; no static white shadow).
   ⚠ `.ulink.prism-text` FOR THE SAME REASON AS THE LADDER ABOVE (forage-fk5): /leaderboard puts
   this on a userLink (page-leaderboard.js), and `color:transparent` here loses to
   `.ulink{color:var(--accent)}` — every crown holder's name went flat accent, spectrum gone. */
.prism-text,.ulink.prism-text{background:var(--prism);background-attachment:scroll;background-size:var(--prism-span);background-position:var(--sheet-pos,0 0);
  -webkit-background-clip:text;background-clip:text;color:transparent;
  /* PERF (owner 2026-07-20q): glow STATIC (cycling filter keyframes repaint per frame);
     FIXED-PX spectrum density so small elements show the full rainbow, not one hue. */
  filter:drop-shadow(0 0 6px rgba(255,255,255,.5)) drop-shadow(0 0 14px rgba(170,215,255,.35))}

/* APEX FRAME — NO SOLID FILL (owner ruling 2026-07-20i). The card keeps the NORMAL panel
   background; the prismatic lives ONLY in: (1) a masked-ring border overlay (.prismring),
   (2) the hue-cycling corner molding, (3) the prism TEXT glow, (4) the saturated .prismglow
   halo. Site-wide idiom — every apex speaks it. */
.apex-frame{position:relative;border-radius:6px}
/* NO isolation (2026-07-20t): the corona's z-index:-1 must escape to the WALL's
   stacking context so it paints BENEATH every sibling card's panel — adjacent
   coronas then blend in the gaps instead of clipping hard against neighbor
   boxes ("the glow clips into other glows"). Requires .apex-frame to NOT be a
   stacking context: no isolation / z-index / filter on the card itself. */
/* (1) the RING: a gradient overlay masked to a 1.5px ring (mask-composite exclude) with
   colored cycling drop-shadows — the border, never a fill. */
.apex-frame > .prismring{position:absolute;inset:-1px;padding:1.5px;pointer-events:none;border-radius:inherit;
  /* ring: the WHITENED spectrum (2026-07-20v) — white-leaning with clear color. */
  background:var(--prism-lite);background-attachment:scroll;background-size:var(--prism-span);background-position:var(--sheet-pos,0 0);
  -webkit-mask:linear-gradient(#fff 0 0) content-box,linear-gradient(#fff 0 0);-webkit-mask-composite:xor;
  mask:linear-gradient(#fff 0 0) content-box,linear-gradient(#fff 0 0);mask-composite:exclude;
  filter:drop-shadow(0 0 5px rgba(180,220,255,.6)) drop-shadow(0 0 14px rgba(255,180,240,.35))}
/* (4) the saturated CORONA — TWO LAYERS (owner ruling 2026-07-20j #2): CSS masks apply AFTER
   filters, so blurring a masked ring re-gains hard edges. Fix: the inner .pg owns the animated
   saturated gradient + the ring mask (content-box/exclude); the PARENT .prismglow owns the blur
   — applied LAST over the already-masked ring, so light truly feathers outward in every
   direction (interior stays effectively clean). */
.apex-frame > .prismglow{position:absolute;inset:-16px;z-index:-1;pointer-events:none;animation:prismbreathe 5s ease-in-out infinite alternate}
@keyframes prismbreathe{from{opacity:.30}to{opacity:.44}}
/* CONIC ×3 — the FINAL corona (owner ruling, end of iterations): linear slides have
   irreducible luminance drift (bright hues blaze, purple recedes → off-center bloom). Fixed
   BY CONSTRUCTION: a conic gradient centered on the card repeating the saturated spectrum ×3
   around the circle, spun via a registered @property angle. Every edge holds a FULL spectrum
   cycle at every moment → rotationally even; the spin animates hue travel. Ring mask + the
   blur-last parent .prismglow are unchanged. No @property support → a static conic (fine).
   Supersedes the dual-phase linear approach entirely. */
.apex-frame > .prismglow > .pg{position:absolute;inset:0;overflow:hidden;
  /* FEATHERED WINDOW (2026-07-20u): the old ring mask cut the pre-blurred strip
     with razor edges — adjacent coronas met as hard bands ("still clipping").
     Coronas paint BENEATH all cards now, so no interior cutout is needed (the
     card body covers the middle); the mask is just soft 16px ramps on every
     edge, so glows FADE out and neighbors blend instead of colliding. */
  -webkit-mask:linear-gradient(to right,transparent,#fff 16px,#fff calc(100% - 16px),transparent),
               linear-gradient(to bottom,transparent,#fff 16px,#fff calc(100% - 16px),transparent);
  -webkit-mask-composite:source-in;
  mask:linear-gradient(to right,transparent,#fff 16px,#fff calc(100% - 16px),transparent),
       linear-gradient(to bottom,transparent,#fff 16px,#fff calc(100% - 16px),transparent);
  mask-composite:intersect}
/* THE STRIP IS NOW THE SHEET, AND THE BLUR IS GONE (2026-07-27). A blurred layer must be
   re-rasterised, and twelve of them was the other half of the mobile cost. The corona reads
   the same shared fixed spectrum as the ring and the text, so a card's halo is the same hue
   as its border — they used to drift apart, being two independent animations. Softness comes
   from the feather mask on .pg plus a low alpha, not from filter:blur. */
.apex-frame .pg .strip{position:absolute;inset:0;
  background:var(--prism-sat);background-attachment:scroll;background-size:var(--prism-span);background-position:var(--sheet-pos,0 0);
  opacity:.42}


/* (2) corner molding brackets (all four), hue-cycling. */
.apex-frame::before,.apex-frame::after,.apex-frame > .corner{content:"";position:absolute;width:14px;height:14px;
  pointer-events:none;filter:drop-shadow(0 0 4px rgba(255,255,255,.5))} /* static — perf */
.apex-frame::before{top:-1px;left:-1px;border-top:2px solid #ffd9e8;border-left:2px solid #ffd9e8}
.apex-frame::after{bottom:-1px;right:-1px;border-bottom:2px solid #d4f3ff;border-right:2px solid #d4f3ff}
.apex-frame > .corner.tr{top:-1px;right:-1px;border-top:2px solid #d9ffe3;border-right:2px solid #d9ffe3}
.apex-frame > .corner.bl{bottom:-1px;left:-1px;border-bottom:2px solid #e2d9ff;border-left:2px solid #e2d9ff}

/* the apex CHIP (tier chip / roster / badges): transparent, clipped cycling text, the
   BORDER + shadow keyframed through the pastel hexes (no white fill/border). */
@keyframes prismchip{
  0%{border-color:#eafff0;box-shadow:0 0 9px rgba(166,255,196,.5)}
  25%{border-color:#e8f7ff;box-shadow:0 0 9px rgba(159,226,255,.5)}
  50%{border-color:#ffeaf7;box-shadow:0 0 9px rgba(255,176,230,.5)}
  75%{border-color:#fffbe6;box-shadow:0 0 9px rgba(255,245,171,.5)}
  100%{border-color:#eafff0;box-shadow:0 0 9px rgba(166,255,196,.5)}}
/* PERF NOTE (2026-07-20q, SETTLED 2026-07-27 — owner reported mobile lag).
   The 2026-07-20q pass claimed the remaining slides "repaint tiny areas". That was wrong for
   the ring: it is inset around the WHOLE row, so each one repainted a full-width box every
   frame. The crowns page ran 24 repainting layers (a ring + a name slide on each of 12 rows)
   plus 12 filter:blur coronas, under a FULL-SCREEN blur(36px) backdrop that was itself
   repainting every frame from the aura's colour slide. Measured A/B on a phone before
   shipping. (That slide is GONE as of 2026-07-28k — the aura is a static pastel glow now;
   the aura was REMOVED outright on 2026-07-28p after the owner measured it on the phone.
   This file no longer animates background-position anywhere.)
   THE RULE NOW, and there are no exceptions left in this file:
     · a prismatic surface samples THE SHEET — var(--prism*) + background-attachment:fixed +
       background-size:var(--prism-span). It does NOT animate.
     · motion is scrolling past the sheet (free), or transform/opacity if a surface genuinely
       needs its own (both compositor-only).
     · never animate background-position, and never filter:blur a layer that repeats per item.
   A new prismatic effect that follows this costs nothing regardless of how many appear. */
/* Only .prismglow still animates (opacity breathe); the rest are static sheets now. */
@media(prefers-reduced-motion:reduce){.apex-frame > .prismglow{animation:none}}
.prism-burst{background:radial-gradient(circle,rgba(255,255,255,.6),transparent 70%)}

/* TITLE tier chips — DASHED, colored by band; the apex chip is prismatic (apex-frame). */
.titles-row{display:flex;flex-wrap:wrap;gap:10px}
.title-chip{position:relative;display:flex;align-items:center;gap:10px;padding:9px 13px;border-radius:6px;
  border:1px dashed var(--hair-hi);background:var(--panel)}
.title-chip.tier-common{border-color:rgba(154,161,173,.5);color:var(--r-common)}
.title-chip.tier-uncommon{border-color:rgba(95,196,107,.5);color:var(--r-uncommon)}
.title-chip.tier-rare{border-color:rgba(77,184,255,.55);color:var(--r-rare);box-shadow:0 0 12px rgba(77,184,255,.12)}
.title-chip.tier-epic{border-color:rgba(217,102,232,.55);color:var(--r-epic);box-shadow:0 0 12px rgba(217,102,232,.14)}
.title-chip.tier-legendary{border-color:rgba(255,217,138,.6);color:var(--r-legendary);box-shadow:0 0 14px rgba(255,217,138,.16)}
.title-chip.tier-mythic{border-color:rgba(255,77,94,.6);color:var(--r-mythic);box-shadow:0 0 14px rgba(255,77,94,.18)}
/* the apex tier CHIP (owner ruling 2026-07-20i): normal panel bg, a SOLID border + shadow
   keyframed through the pastel hexes (prismchip), the NAME clipped-cycling (.prism-text).
   No fill, no ring/glow/corners — that fuller frame is for cards (leaderboard roster). */
.title-chip.prismchip{background:var(--panel);border:1px solid #eafff0;color:#e8ecff;animation:prismchip 6s linear infinite}
@media(prefers-reduced-motion:reduce){.title-chip.prismchip{animation:none;border-color:#d4f3ff}}
.title-ic{font-size:22px;line-height:1;filter:drop-shadow(0 0 6px currentColor)}
.title-body{display:flex;flex-direction:column;gap:2px}
.title-nm{font-size:12px;letter-spacing:calc(.06em * var(--track));color:currentColor}
.title-since{font-size:9px;letter-spacing:calc(.05em * var(--track));color:var(--faint);text-transform:var(--caps)}
.title-unlock{animation:aw-pop .5s ease}
@keyframes aw-pop{0%{transform:scale(.7);opacity:.3}60%{transform:scale(1.12)}100%{transform:scale(1);opacity:1}}
/* leaderboard crowns roster — each axis's APEX holder in prismatic flare.
   ⛔ `.roster-h` (+ its 👑 ::before) and `.roster-grid` ARE GONE: f691a777 hands the heading to
   `.pn-tab` and the grid to `.pn-cells .pns-lb-crowns`. `.roster-cell` and everything under it
   is STILL DRAWN by page-leaderboard.js inside that cell, so it all stays. */
/* ⚠ THE CELL IS THE GRID ITEM, NOT THE ROW. The panel has to open UNDER ITS OWN CROWN, and
   this grid is multi-column on a desktop — a panel appended as a sibling of .roster-row would
   land in the NEXT COLUMN and explain the wrong crown. Wrapping each crown in a cell keeps
   row+panel together at any column count. */
.roster-cell{display:flex;flex-direction:column;gap:0;min-width:0}
/* ⚠ NO flex:1 ON THE ROW. Equal heights come from the two-line row shape (nowrap + ellipsis
   on both lines), NOT from stretching — a stretched row balloons every neighbor of an OPEN
   crown panel into a mostly-empty box the full panel height. */
.roster-cell .roster-row{cursor:pointer}
.crownpanel{display:grid;grid-template-columns:auto 1fr;gap:4px 10px;align-items:baseline;
  padding:9px 11px 10px;border:1px solid var(--hair);border-top:none;font-size:11px}
.cp-k{color:var(--faint);text-transform:var(--caps);letter-spacing:calc(.1em * var(--track));font-size:9.5px;white-space:nowrap}
.cp-v{color:var(--ink);min-width:0;overflow-wrap:anywhere}
.cp-dim{color:var(--dim)} .cp-mid{align-self:center}
.cp-post{display:inline-flex;align-items:center;gap:7px}
.cp-thumb{width:34px;height:34px;object-fit:cover;border:1px solid var(--hair)}
.cp-rating{color:var(--bad);font-weight:600}
/* TWO LINES BY DESIGN (owner report 2026-08-01): icon | title-over-holder, the .title-chip
   idiom. The old single line ("title …… holder", space-between) wrapped mid-word whenever a
   long title met a long name, and only that card grew — the wall read as ragged boxes. Now
   every card is the same two-line shape and the holder name gets the full card width. */
.roster-row{display:flex;align-items:center;gap:11px;padding:8px 12px;border-radius:6px;font-size:12px;min-width:0}
.roster-row.vacant{border:1px dashed var(--hair);background:none}
.roster-ic{font-size:19px;line-height:1;flex-shrink:0}
.roster-body{display:flex;flex-direction:column;gap:3px;min-width:0;flex:1;justify-content:center}
.roster-title{letter-spacing:calc(.05em * var(--track));color:var(--dim);font-size:11px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.roster-row.vacant .roster-title{color:var(--faint)}
.roster-holder{font-weight:600;align-self:flex-start;max-width:100%;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.roster-holder.vacant{color:var(--faint);font-style:italic;font-weight:400}
/* recap title lines — a gained crown (an apex change) reads prismatic-adjacent. */
.recap-line.recap-gain{color:#e2d9ff} .recap-line.recap-loss{color:var(--dim)}

/* THE APEX — the reigning #1 by score, PRISMATIC EVERYWHERE (owner ruling 2026-07-20f:
   all apexes speak one prismatic language; gold is now legendary-medal only). */
.apex-crown{-webkit-text-fill-color:initial;filter:drop-shadow(0 0 4px #fff);margin-right:.32em}
/* the crown REPLACES the rank pip for the apex (owner ruling 2026-07-27), so it inherits
   the pip's right-hand gap — without it the name crowds the crown. */
/* apex NAME: clipped-cycling spectrum + STATIC glow (perf 2026-07-20q). */
.ulink.apex-name,.prof-name.apex-name{
  background:var(--prism);background-attachment:scroll;background-size:var(--prism-span);background-position:var(--sheet-pos,0 0);
  -webkit-background-clip:text;background-clip:text;color:transparent;
  filter:drop-shadow(0 0 6px rgba(255,255,255,.5)) drop-shadow(0 0 14px rgba(170,215,255,.35))}

/* ============ THE TWO CLASSES — a KIND of account, not a rung on the ladder ================
   docs/SPEC_scion.md v2 §3.6: "a class renders in its own colour everywhere a username
   renders". web/common.js's userLink() puts `.binkie-name` OR `.scion-name` on the ONE element
   every username is, so this is the whole treatment; .prof-name rides along exactly as
   .apex-name does.

   ⚖ WHY THEY ARE CLASSES AND NOT TWO MORE .rk-<tier>s. The tiers ARE a ladder — userLink
   derives them from opts.score — so either of these added to that palette would be read as
   "ranked between freeman and noble", which is precisely the wrong sentence. They sit beside
   the apex, which is the repo's existing precedent for a per-user visual class that score
   cannot produce.

   ⚖ ONE ELEMENT, ONE COLOUR, AND THE COLLAPSE IS IN userLink — NOT HERE. An account may be
   BOTH a binkie and a scion (they are independent booleans), and when it is, the name is GREEN:
   the trust class governs the account's place in the tree and its permissions, and mistaking a
   quarantined account for an ordinary content-shielded one is the worse of the two errors. That
   decision is made ONCE, in the client, which is why these two rules never have to fight — do
   not "fix" a both-flags account by adding a `.binkie-name.scion-name` rule here.

   ⚠ THE TWO-CLASS SELECTOR IS LOAD-BEARING, and it is why .rk-<tier> does not actually paint a
   username today. `.ulink{color:var(--accent)}` lives in app-profile.css, which loads AFTER
   this file, and a single-class rule (0,1,0) loses that tie on source order — which is how
   `.ulink.apex-name` (0,2,0) ended up the only identity colour that survives. Written as
   `.scion-name` alone this would silently do nothing on every link in the app. Do not "tidy"
   the .ulink off the front of these selectors. The :hover pair is the same story one step up
   (`.ulink:hover` is also 0,2,0 and also later), matching the `.ulink.rk-*:hover` row above.

   ⚖ THEY WIN OVER THE APEX, deliberately, and that is why they sit AFTER it at equal weight: a
   rank is decoration and a class is what the account IS — the crown glyph still renders, so
   the position is not lost, only its paint. Beating the prismatic name means undoing all three
   of its properties (it is background-clip:text over color:transparent, so a bare `color:` on
   top of it renders nothing at all), which is what the first three declarations are for. */
.ulink.binkie-name,.prof-name.binkie-name{
  background:none;-webkit-background-clip:border-box;background-clip:border-box;
  color:var(--binkie);filter:none;
  /* the glow and the hover are DERIVED from the var, not two more hexes — "one source of truth"
     stops being true the moment the halo has to be edited alongside the colour. */
  text-shadow:0 0 9px color-mix(in srgb, var(--binkie) 35%, transparent)}
.ulink.binkie-name:hover{color:color-mix(in srgb, var(--binkie) 72%, white)}
.ulink.scion-name,.prof-name.scion-name{
  background:none;-webkit-background-clip:border-box;background-clip:border-box;
  color:var(--scion);filter:none;
  text-shadow:0 0 9px color-mix(in srgb, var(--scion) 35%, transparent)}
.ulink.scion-name:hover{color:color-mix(in srgb, var(--scion) 72%, white)}
/* LIGHT-SKIN CORRECTION: both are dark-UI colours and wash out on a light page. Kept here
   rather than in app-skins.css so each hue and its correction stay one edit apart — the
   html[data-light] prefix carries enough specificity to win from anywhere in the cascade. */
html[data-light="1"] .ulink.binkie-name,html[data-light="1"] .prof-name.binkie-name{
  color:var(--binkie-lo);text-shadow:none}
html[data-light="1"] .ulink.binkie-name:hover{color:color-mix(in srgb, var(--binkie-lo) 78%, black)}
html[data-light="1"] .ulink.scion-name,html[data-light="1"] .prof-name.scion-name{
  color:var(--scion-lo);text-shadow:none}
html[data-light="1"] .ulink.scion-name:hover{color:color-mix(in srgb, var(--scion-lo) 78%, black)}

/* THE APEX PILL — the ONE compact standing signal, beside the name (owner ask 2026-08-08).
   It takes the apex language (spectrum text, prismchip hairline) at chip scale, so the crown
   glyph and this pill say "reigning #1" once between them instead of three times.
   ⚖ `.prof-badge.apex-pill` is (0,2,0) and beats `.prof-badge` (0,1,0) on specificity, so it
   does NOT depend on this file loading before app-profile.css — which it does, which would
   otherwise lose the tie. Same reasoning as `.ulink.apex-name` above.
   The `.apex-ribbon` rules directly below are KEPT ON PURPOSE even though nothing renders that
   element right now: the reversal is one line (APEX_BANNER in buildProfileView), and it stops
   being one line the moment its stylesheet is deleted too. */
.prof-badge.apex-pill{border-color:#eafff0;font-weight:700;
  background:var(--prism);background-attachment:scroll;background-size:var(--prism-span);
  background-position:var(--sheet-pos,0 0);
  -webkit-background-clip:text;background-clip:text;color:transparent}
/* apex RIBBON: NO fill (owner ruling 2026-07-20i) — clipped-cycling text + prismchip border. */
.apex-ribbon{text-align:center;font-size:11px;letter-spacing:calc(.28em * var(--track));text-transform:var(--caps);font-weight:700;
  background:var(--prism);background-attachment:scroll;background-size:var(--prism-span);background-position:var(--sheet-pos,0 0);
  -webkit-background-clip:text;background-clip:text;color:transparent;
  padding:8px;margin:0 0 16px;border-radius:3px;border:1px solid #eafff0;
  animation:prismchip 6s linear infinite}
@media(prefers-reduced-motion:reduce){.apex-ribbon{animation:none}} /* names are static sheets now */

/* recent — lowercase one-liners; the whole line links to the post */
.dsr-events{display:flex;flex-direction:column;gap:6px}
.dsr-ev{display:flex;align-items:baseline;gap:7px;font-size:10.5px;color:var(--dim)}
a.dsr-ev:hover{color:var(--bright)}
.dsr-ev-t{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.dsr-ev-d{color:var(--accent);font-variant-numeric:tabular-nums}
.dsr-ev-a{color:var(--faint);white-space:nowrap}

.avatarctl{margin-top:10px}

/* the score scale-pop as the dot lands (1 -> 1.25 -> 1) */
.rail-score.pop,.scorechip-n.pop{animation:scorepop .2s ease-out}
@keyframes scorepop{0%{transform:scale(1)}45%{transform:scale(1.25)}100%{transform:scale(1)}}

/* THE FLYING MARK — the drawing it lands on, never a round blob (a disc was the COIN's shape). ⚠ The margin is HALF the box, negative: the element is positioned from `top:0;left:0` by a JS transform, so that margin is the only thing putting the INK under the point it flies from. It moves with `.scoremark` — 13->16px without -6.5->-8px flies every score 1.5px off its own target. */
.pointdot{position:fixed;top:0;left:0;width:16px;height:16px;margin:-8px 0 0 -8px;
  color:var(--accent);fill:currentColor;z-index:var(--z-juice);pointer-events:none;
  filter:drop-shadow(0 0 3px rgba(237,167,51,.9))}

/* the "+N" floater that rises + fades at the interaction point */
.pointfloat{position:fixed;z-index:var(--z-juice);pointer-events:none;transform:translate(-50%,-50%);
  color:var(--accent);font-variant-numeric:tabular-nums;font-size:14px;font-weight:600;
  letter-spacing:calc(.04em * var(--track));text-shadow:0 1px 3px var(--void);animation:pointrise .75s ease-out forwards}
@keyframes pointrise{0%{opacity:0;transform:translate(-50%,-40%)}
  15%{opacity:1}100%{opacity:0;transform:translate(-50%,-190%)}}

/* rank-up: a single rail flash (no confetti — deadpan) */
.rail.rankup{animation:railflash .6s ease-out}
@keyframes railflash{0%{box-shadow:0 0 0 1px var(--void),0 8px 30px rgba(0,0,0,.5)}
  30%{box-shadow:0 0 0 1px var(--accent),0 0 24px 4px rgba(237,167,51,.55)}
  100%{box-shadow:0 0 0 1px var(--void),0 8px 30px rgba(0,0,0,.5)}}

@media(prefers-reduced-motion:reduce){.rail-score,.scorechip-n,.rail.rankup{animation:none}
  .rail-fill{transition:none}
  .pointdot,.pointfloat{display:none!important}
}

/* ---- auth cards ---- */
.authcard{max-width:380px;margin:8vh auto 0;border:1px solid var(--hair);background:var(--panel);padding:22px}
.authcard h2{font-size:13px;letter-spacing:calc(.18em * var(--track));text-transform:var(--caps);color:var(--bright);margin-bottom:4px;font-weight:400}
.authcard .sub{font-size:11px;color:var(--dim);margin-bottom:16px}
.authcard label{display:block;font-size:10px;letter-spacing:calc(.14em * var(--track));text-transform:var(--caps);color:var(--dim);margin:12px 0 4px}
.authcard button.go{width:100%;margin-top:18px;border:1px solid var(--accent);color:var(--accent);padding:9px;letter-spacing:calc(.12em * var(--track));text-transform:var(--caps);font-size:11px}
.authcard .alt{margin-top:14px;font-size:11px;color:var(--faint);text-align:center}
/* ⚖ THE WAY IN IS NOT A 13px LINK (forage-69n, 2026-08-19). `have an invite? register →` measured 13px tall — the SMALLEST control in the archive, on the only screen a stranger ever sees, beside a 38px input and a 37px submit. It stays the same quiet 11px sentence and grows a real box around itself; the card's own margin gives most of the height back so the line barely moves. ⚠ The label's 4px gap to its input goes with it: a `label[for]` IS a control (tapping it focuses the field), and two targets 4px apart share a fingertip — 8px is the same TAP_GAP floor the sort row now keeps. COARSE ONLY, per --tap-min's rule in app.css: on a mouse this is an ordinary text link. */
@media(pointer:coarse){ .authcard .alt{margin-top:4px} .authcard .alt a{display:inline-flex;align-items:center;justify-content:center;min-height:var(--tap-min);min-width:var(--tap-min);padding:0 12px} .authcard label{margin:12px 0 8px} .authcard input,.authcard button.go{min-height:var(--tap-min)} }   /* the two fields and the submit were 38/37 — a 6px round-up measured on the same coarse-pointer run, taken while the card was open rather than left one bead behind the link beside them. The <label> is still 16px and is NOT lifted: it is a 10px caps word, and 44px of it is a design question, not a repair. */

/* ---- PWA SHARE TARGET (web/share-target.html) --------------------------------------
   The screen you land on after sharing a link into the archive from the Android share
   sheet. It is ONLY ever seen on a PHONE, and it is the archive's entire first
   impression after a share — so it is a status SCREEN, not a form response.
   THE PROGRESS BAR IS HONEST: its position is a REAL phase read from
   GET /api/imports/:id (sent -> queued/running -> archived). It never creeps and never
   invents a percentage; the travelling SHEEN is what says "working, length unknown"
   (same ruling as .bld-progress). The checkmark is a DRAWN svg stroke, not a glyph.
   The points juice is common.js's shared applyPoints() — no second celebration path. */
.sharecard{max-width:420px;margin:6vh auto 0;text-align:center}
.sharecard h2{margin-bottom:10px}
.sh-host{display:inline-block;max-width:100%;overflow-wrap:anywhere;font-size:12px;
  color:var(--accent);border:1px solid var(--hair);background:var(--sunk);padding:4px 9px;margin-bottom:16px}
.sh-medal{position:relative;width:92px;height:92px;margin:2px auto 14px}
.sh-medal svg{width:100%;height:100%;display:block;overflow:visible}
.sh-ring{fill:none;stroke:var(--hair-hi);stroke-width:3}
.sh-arc{fill:none;stroke:var(--accent);stroke-width:3;stroke-linecap:round;
  stroke-dasharray:48 220;transform-origin:50% 50%;animation:sh-spin 1.15s linear infinite}
@keyframes sh-spin{to{transform:rotate(360deg)}}
/* 80 comfortably exceeds the mark path's real length (~65u in the 100x100 viewBox), so a
   full dashoffset hides it completely and the keyframe reveals it stroke-first. */
.sh-mark{fill:none;stroke:var(--good);stroke-width:6;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:80;stroke-dashoffset:80}
.sh-medal.drawn .sh-mark{animation:sh-draw .44s cubic-bezier(.2,.75,.3,1) forwards}
@keyframes sh-draw{to{stroke-dashoffset:0}}
.sh-medal.done .sh-ring{stroke:var(--good)}
.sh-medal.bad .sh-ring{stroke:var(--bad)}
/* the ✕ is a longer path than the ✓ (~91u vs ~65u across two subpaths), so it needs its
   own dash length or the tail of the second stroke would already be showing. */
.sh-medal.bad .sh-mark{stroke:var(--bad);stroke-dasharray:104;stroke-dashoffset:104}
.sh-halo{position:absolute;inset:0;border-radius:50%;border:2px solid var(--good);opacity:0;pointer-events:none}
.sh-medal.done .sh-halo{animation:sh-halo .75s ease-out .12s}
@keyframes sh-halo{0%{opacity:.85;transform:scale(.68)}100%{opacity:0;transform:scale(1.55)}}
.sh-line{font-size:14px;color:var(--bright);margin-bottom:12px;overflow-wrap:anywhere}
.sh-track{position:relative;height:6px;background:var(--sunk);border:1px solid var(--hair);overflow:hidden}
.sh-fill{height:100%;width:0;background:var(--accent);transition:width .45s cubic-bezier(.3,.85,.3,1)}
.sh-track.done .sh-fill{background:var(--good)}
.sh-track.bad .sh-fill{background:var(--bad)}
.sh-track.busy::after{content:"";position:absolute;inset:0;
  background:linear-gradient(90deg,transparent,rgba(237,167,51,.45),transparent);
  transform:translateX(-100%);animation:sh-sheen 1.5s ease-in-out infinite}
@keyframes sh-sheen{to{transform:translateX(100%)}}
.sh-steps{display:flex;justify-content:space-between;margin:9px 0 12px;font-size:10px;
  letter-spacing:calc(.1em * var(--track));text-transform:var(--caps);color:var(--faint)}
.sh-step{display:flex;align-items:center;gap:5px}
.sh-step::before{content:"";width:6px;height:6px;border-radius:50%;border:1px solid var(--hair-hi)}
.sh-step.on{color:var(--accent)}   .sh-step.on::before{border-color:var(--accent);background:var(--accent)}
.sh-step.past{color:var(--dim)}    .sh-step.past::before{border-color:var(--good);background:var(--good)}
.sh-step.bad{color:var(--bad)}     .sh-step.bad::before{border-color:var(--bad);background:var(--bad)}
.sharecard .banner{text-align:center;margin:10px 0 0}
.sh-award{font-size:12px;color:var(--good);margin-top:8px;min-height:15px}
/* >=44px touch target: this screen is phone-only (CLAUDE.md MOBILE FIRST). */
.sh-go{display:flex;align-items:center;justify-content:center;min-height:46px;margin-top:16px;
  padding:0 12px;border:1px solid var(--accent);color:var(--accent);text-decoration:none;
  font-size:12px;letter-spacing:calc(.1em * var(--track))}
.sh-go.quiet{border-color:var(--hair-hi);color:var(--dim)}
/* quick tags under the success card — tag the fresh import without chasing the post.
   The input needs the same >=44px touch height as .sh-go (phone-only screen). */
.sh-quicktag{margin-top:18px;text-align:left}
.sh-quicktag .hint{text-align:center;margin-bottom:7px}
.sh-quicktag .tagadd input{min-height:44px}
.sh-quicktag .tagadd-go{min-width:46px}
.sh-quicktag .taglist{margin-top:8px;justify-content:center}
.sh-tagerr{color:var(--bad);margin-top:5px;min-height:0}
@media(prefers-reduced-motion:reduce){
  /* the arc keeps its gap so it still reads as a partial ring — it just stops turning. */
  .sh-arc{animation:none}
  .sh-medal.drawn .sh-mark{animation:none;stroke-dashoffset:0}
  .sh-medal.done .sh-halo{animation:none}
  .sh-track.busy::after{animation:none;display:none}
  .sh-fill{transition:none}
}

/* ---- confirm modal (mint-as-vouch liability, invite_lineage v1) ---- */
.modal-backdrop{position:fixed;inset:0;z-index:var(--z-modal);background:rgba(0,0,0,.62);display:flex;align-items:center;justify-content:center;padding:20px}
.modal-card{max-width:420px;width:100%;border:1px solid var(--accent);background:var(--panel);
  padding:12px;box-shadow:0 0 0 1px var(--hair),0 18px 50px rgba(0,0,0,.55)}
.modal-card h2{font-size:12px;letter-spacing:calc(.18em * var(--track));text-transform:var(--caps);color:var(--accent);
  margin-bottom:10px;font-weight:400}
.modal-card h2::before{content:"⚠ "}
.modal-card .mbody{font-size:12px;line-height:1.6;color:var(--dim);white-space:pre-wrap}
.modal-card .mbody b{color:var(--bright);font-weight:600}
.modal-acts{display:flex;gap:10px;margin-top:20px}
.modal-acts button{flex:1;padding:9px;letter-spacing:calc(.1em * var(--track));text-transform:var(--caps);font-size:11px}
/* ⚖ 44px OF DIALOG BUTTON, AND THE DRAWN BUTTON GROWS WITH IT (forage-fxj, 2026-08-20) — the one
   place in this sweep where the band idiom would be WRONG. These are the app's yes/no on seven
   surfaces (common-audio, common-chrome, page-post-tags/-sections/-actions, xp-search, xp-trim),
   the yes is regularly the irreversible half, and a mis-tap is not a missed tap but the WRONG
   ANSWER to a confirmation — so the hit area and the visible target must be one rectangle. Free:
   `.modal-card` is content-sized in a centred flex backdrop, so the card grows 9px and the page
   does not move, and the existing gap:10px already clears the 8px adjacent-control minimum. */
@media(pointer:coarse){ .modal-acts button{min-height:var(--tap-min)} }
.modal-acts .yes{border:1px solid var(--accent);color:var(--accent)}
.modal-acts .no{border:1px solid var(--hair);color:var(--faint)}

/* ---- THE KEY SCREEN (web/common-triage.js inviteKeyScreen) -------------------------------
   The full-screen receipt raised when an invite key is minted, and re-opened from the code on
   /admin's invites panel or your own profile's keys tab. Owner ask, 2026-09-09: *"a full screen
   pop up … the code, a copyable link with the code, and other stats about the code like current
   status … kind of like the link import page with the scan bar and circle and stuff."*

   ⚖ THE ANATOMY IS `.sharecard`'S, ONE SHEET UP: the centred card, the medallion, the state
   line, the row of named step dots. Same vocabulary, deliberately — this is the archive's
   settled "one act, one screen" shape and a second idiom for it would be a second answer.
   ⛔ AND ITS MOTION IS NOT BORROWED, WHICH IS THE WHOLE DIFFERENCE. `.sh-arc` turns and
   `.sh-track.busy` sheens because an import is WORK IN FLIGHT and the client cannot see how
   much is left. AN INVITE HAS NO PROGRESS — it sits exactly where it was minted until somebody
   uses it, which may be weeks. So: no track at all (a fill at any width claims a fraction that
   does not exist), no spin, and the check is drawn at its final offset rather than struck on.
   ⚖ THAT IS ALSO WHY THIS BLOCK NEEDS NO prefers-reduced-motion BRANCH: there is nothing here
   that moves, at any width, in any state. ⛔ Do not add one "for symmetry" — an empty media
   query reads as coverage.
   ⚑ THE FRAME IS ACCENT, like `.modal-card` and unlike `.authcard`. An unredeemed code is a
   live registration credential against this archive's entire moderation perimeter; the card it
   is printed on says so with its edge. ⛔ Never `--bad`: a key waiting to be used is the system
   working (docs/surfaces/admin.md, "no red badge for an ordinary count"). */
.ikey-screen{position:fixed;inset:0;z-index:var(--z-modal);background:rgba(0,0,0,.85);
  display:flex;align-items:flex-start;justify-content:center;overflow-y:auto;
  padding:max(16px,env(safe-area-inset-top)) 14px max(16px,env(safe-area-inset-bottom))}
.ikey-card{width:100%;max-width:420px;margin:auto;border:1px solid var(--accent);
  background:var(--panel);padding:20px 16px;text-align:center;
  box-shadow:0 0 0 1px var(--hair),0 18px 50px rgba(0,0,0,.55)}
.ikey-title{font-size:12px;letter-spacing:calc(.18em * var(--track));text-transform:var(--caps);
  color:var(--accent);font-weight:400;margin-bottom:14px}

/* THE MEDALLION — the circle the owner named. Waiting is the hairline ring plus a STATIC gapped
   arc, which is exactly the shape `.sh-arc` falls back to under reduced motion; used swaps the
   ring to --good and adds the drawn check. `overflow:visible` because the 3px stroke sits on
   the r=40 path and would otherwise clip at the viewBox edge. */
.ikey-medal{width:84px;height:84px;margin:0 auto 14px}
.ikey-medal svg{width:100%;height:100%;display:block;overflow:visible}
.ikey-ring{fill:none;stroke:var(--hair-hi);stroke-width:3}
.ikey-arc{fill:none;stroke:var(--accent);stroke-width:3;stroke-linecap:round;
  stroke-dasharray:48 220;transform:rotate(-90deg);transform-origin:50% 50%}
.ikey-medal.is-used .ikey-ring{stroke:var(--good)}
/* ⛔ AND A KEY THAT IS GONE GETS NO COLOUR AT ALL (forage-xa68.5). --good would read as
   "done" and --bad as "something broke": a revoked key is neither, it is an ABSENCE, and the
   ring drops to the hairline it would have had before anything happened. The accent arc is
   removed by markGone() rather than hidden here, because a gapped arc under a dead key is the
   "waiting" reading this screen's own header refuses to fake. */
.ikey-medal.is-gone .ikey-ring{stroke:var(--hair)}
.ikey-gone{margin:0;color:var(--dim)}
/* dashoffset 0, and no keyframe: see the block header. 80 exceeds the path's ~65u length, so
   the dasharray is inert here and exists only so this rule reads the same as `.sh-mark`. */
.ikey-mark{fill:none;stroke:var(--good);stroke-width:6;stroke-linecap:round;
  stroke-linejoin:round;stroke-dasharray:80;stroke-dashoffset:0}

.ikey-line{font-size:15px;color:var(--bright);letter-spacing:calc(.04em * var(--track))}
.ikey-by{font-size:12px;color:var(--dim);margin-top:5px;overflow-wrap:anywhere}
/* THE STEPS. Three dots and no bar between them: the reading `.sh-steps` gives, without the
   completion arithmetic `.sh-fill` implies. The row is capped and centred so three short words
   do not stretch to 420px of gap. */
.ikey-steps{display:flex;justify-content:space-between;gap:8px;max-width:280px;
  margin:14px auto 16px;font-size:10px;letter-spacing:calc(.1em * var(--track));
  text-transform:var(--caps);color:var(--faint)}
.ikey-step{display:flex;align-items:center;gap:5px}
.ikey-step::before{content:"";width:6px;height:6px;border-radius:50%;border:1px solid var(--hair-hi)}
.ikey-step.on{color:var(--accent)}   .ikey-step.on::before{border-color:var(--accent);background:var(--accent)}
.ikey-step.past{color:var(--dim)}    .ikey-step.past::before{border-color:var(--good);background:var(--good)}

/* THE TWO BOXES — the code, and the link that carries it. A hairline-framed sunk panel each,
   with its own caps label: they are two different things to do with one key (read it aloud,
   send it), and the 2026-09-02 ruling is that distinct features get their own card rather than
   another mono-caps row. */
.ikey-box{border:1px solid var(--hair);background:var(--sunk);padding:11px 12px;
  margin-bottom:10px;text-align:left}
.ikey-lbl{font-size:9.5px;letter-spacing:calc(.14em * var(--track));text-transform:var(--caps);
  color:var(--faint);margin-bottom:7px}
/* THE CODE IS THE BIGGEST TEXT ON THE SCREEN, because it is the thing being handed to a person
   — read back over a voice call as often as pasted. `user-select:all` makes one tap take the
   whole string; `overflow-wrap:anywhere` keeps an 8-char base64url token off a 390px edge even
   with the tracking on. */
.ikey-code{font-family:var(--mono,ui-monospace,monospace);font-size:clamp(22px,7vw,30px);
  line-height:1.2;color:var(--accent);letter-spacing:calc(.08em * var(--track));
  overflow-wrap:anywhere;user-select:all;-webkit-user-select:all}
/* ⚖ THE FIELD TAKES A LINE OF ITS OWN AND THE BUTTON SITS UNDER IT — MEASURED, not a
   preference (2026-09-09, 390x844 and 1440x900). Beside a 64px button the field is 232px at
   390px and the URL truncates at `https://archive.shredverse.` — the ORIGIN, with the code
   cut off, on a screen whose whole subject is that code. Given the full width it is ~306px and
   the whole 40-character link is on screen at BOTH widths. ⛔ Widening the card instead is the
   fix that stops working the first time a destination is hung off the link; this one degrades
   to the same truncation the /admin row already documents, and the `· plain link` sentence
   below still says where it goes. Full-width is also the better tap target. */
.ikey-linkrow{display:flex;flex-wrap:wrap;align-items:center;gap:6px}
/* ⚠ 11px, AND IT IS A MEASUREMENT. At 12px the 40-character link overflows its own field by
   one character at 390px — `…/i/qGK` — which is the code, again, cut off. At 11px it fits
   whole at both widths. ⚖ It is the one place on this screen small type is right: this field
   is COPIED, not read, and the code it carries is set at 30px eight lines above it. */
.ikey-url{flex:1 1 100%;min-width:0;font-size:11px;padding-left:7px;padding-right:7px}
.ikey-where{margin:7px 0 0}
.ikey-where.on{color:var(--good)}

/* THE FACTS — only what the invites row records (code, created_at, used_by, used_at, binkie,
   scion). Two columns so the labels line up; the value wraps, because "expires: never — a key
   waits until it is used, or until it is revoked" is a sentence and truncating it would hide
   the half that answers the question. */
.ikey-facts{margin:14px 0 4px;text-align:left;font-size:11px}
.ikey-fact{display:grid;grid-template-columns:5.5em 1fr;gap:10px;align-items:baseline;
  padding:6px 0;border-bottom:1px solid var(--hair)}
.ikey-fact:last-child{border-bottom:none}
.ikey-fk{letter-spacing:calc(.1em * var(--track));text-transform:var(--caps);color:var(--faint)}
.ikey-fv{color:var(--ink);overflow-wrap:anywhere}

/* >=44px, unconditionally, on the same argument `.sh-go` states two blocks up: this screen is
   read on a phone at least as often as anywhere else and these are its only three controls. */
.ikey-copy{flex:1 1 100%;justify-content:center;min-height:var(--tap-min);min-width:64px}
.ikey-url{min-height:var(--tap-min)}
.ikey-done{display:flex;align-items:center;justify-content:center;width:100%;
  min-height:var(--tap-min);margin-top:14px;border:1px solid var(--accent);color:var(--accent);
  font-size:11px;letter-spacing:calc(.12em * var(--track));text-transform:var(--caps)}

/* THE DOOR BACK IN, on both lists. A bare button wrapped around the code that was already
   there, so `.as-code` and `.inv-code` keep their own type and the row keeps its layout — the
   button contributes nothing but a press target. ⛔ 44px on TOUCH ONLY: these sit inside dense
   list rows where a mouse never needed the growth, which is the same shape `.as-urow .as-ubtn`
   keeps one sheet over (and the only shape mobiletaps.test.js credits). */
.ikey-open{background:none;border:none;padding:0;margin:0;font:inherit;color:inherit;
  text-align:left;cursor:pointer;min-width:0}
.ikey-open:hover .as-code,.ikey-open:hover .inv-code{color:var(--bright)}
@media(pointer:coarse){ .ikey-open{min-height:var(--tap-min);display:inline-flex;align-items:center} }


/* the "add to collection…" picker (collections v1) */
.collpicker-row{display:flex;justify-content:space-between;align-items:center;gap:8px;
  padding:7px 0;border-bottom:1px solid var(--hair);font-size:12px;color:var(--ink)}
.collpicker-row:last-child{border-bottom:none}
.collpicker-new{display:flex;gap:6px;margin-top:10px}
.collpicker-new input{flex:1}
.collpicker-msg{margin-top:8px;min-height:14px}

/* POST PICKER v1 (owner ruling 2026-07-21x) — a full-screen search overlay
   (web/common.js openPostPicker), not a small modal: on mobile especially,
   picking a post by search needs real screen space, not a cramped dialog.
   --z-blocking, i.e. above the hover-card rig and the viewer, so it can be
   opened from anywhere including on top of a grown hover-card. picker-open on
   <body> suppresses background scroll while it's up (a full-viewport fixed
   overlay over a scrollable page still lets the page itself scroll underneath
   otherwise, on some mobile browsers). */
body.picker-open{overflow:hidden}
/* padding, not `top:env(...)`: inset:0 is what makes the picker cover the whole screen, and the
   bar is the flex column's first child, so padding is what moves it clear of a notch. Live
   since 2026-08-09 — before viewport-fit=cover this evaluated to 0px like every other env(). */
.pickerlay{position:fixed;inset:0;z-index:var(--z-blocking);background:var(--panel);
  display:flex;flex-direction:column;
  padding:env(safe-area-inset-top,0px) env(safe-area-inset-right,0px) env(safe-area-inset-bottom,0px) env(safe-area-inset-left,0px)}
.picker-bar{display:flex;align-items:center;gap:10px;padding:10px 14px;border-bottom:1px solid var(--hair);flex:0 0 auto}
.picker-close{width:32px;height:32px;flex:0 0 auto;border-radius:6px;border:1px solid var(--hair-hi);
  background:none;color:var(--ink);font-size:13px;cursor:pointer}
.picker-close:hover{border-color:var(--accent);color:var(--accent)}
.picker-title{font-size:12px;letter-spacing:calc(.1em * var(--track));text-transform:var(--caps);color:var(--dim);font-weight:600}
.picker-search{padding:10px 14px;border-bottom:1px solid var(--hair);flex:0 0 auto}
.picker-search input{width:100%;box-sizing:border-box;padding:11px 12px;border-radius:8px;border:1px solid var(--hair-hi);background:var(--sunk);color:var(--ink);font-size:16px} /* 16px: no iOS auto-zoom on focus */
/* ⚑ SLICE 4 — THE SHELF'S TAB ROW. ⚖ The picker gained shortlists rather than growing a second
   picker beside it (the archive's ONE post-pick control, which the member's face and the club's
   picture already go through). ⚠ 44px targets — a tab is a control the screen is FOR, so it takes
   the floor rather than `.sc-react`'s 22px chip exemption. ⛔ Drawn only when a caller supplies
   shelves, so every other picker on the archive is byte-identical to what it was. */
.picker-shelf{display:flex;gap:6px;padding:8px 14px 0;flex:0 0 auto;flex-wrap:wrap}
.picker-shelf a{display:inline-flex;align-items:center;min-height:44px;padding:0 12px;
  border-radius:8px;border:1px solid var(--hair-hi);color:var(--dim);text-decoration:none;
  font-size:11px;letter-spacing:calc(.06em * var(--track));text-transform:var(--caps);cursor:pointer}
.picker-shelf a.on{border-color:var(--accent);color:var(--accent)}
.picker-results{flex:1 1 auto;overflow-y:auto;padding:14px}
.picker-hint{padding:20px 0;text-align:center}
.picker-grid{padding-bottom:20px}
.picker-tile{cursor:pointer;position:relative}
.picker-pick{margin-top:4px;padding:6px 4px;border-radius:5px;background:var(--accent);
  color:#1a1400;font-size:11px;font-weight:700;letter-spacing:calc(.04em * var(--track));text-align:center}

/* HARD DELETE (purge) — a distinct, SCARIER control than soft-delete: deeper red,
   solid border, spaced below the soft-delete button. */
.btn.purge{display:block;margin-top:10px;color:#ff5b5b;border:1px solid #ff5b5b;
  background:rgba(224,85,95,.06);letter-spacing:calc(.12em * var(--track))}
.btn.purge:hover{border-color:#ff8080;background:rgba(224,85,95,.14);color:#ff8080}
.danger-note{margin-top:8px;font-size:11px;line-height:1.5;color:var(--faint)}
/* the purge modal glows red instead of amber; the arm-input + confirm are red. */
.modal-card.purge-modal{border-color:var(--bad);box-shadow:0 0 0 1px #3a2226,0 18px 50px rgba(0,0,0,.6)}
.modal-card.purge-modal h2{color:var(--bad)}
.purge-input{margin-top:6px;border-color:var(--bad);letter-spacing:calc(.1em * var(--track))}
.purge-input:focus{border-color:#ff8080}
.modal-acts .yes.purge{border-color:var(--bad);color:var(--bad)}
.modal-acts .yes.purge:hover:not(:disabled){border-color:#ff8080;color:#ff8080}
.modal-acts .yes.purge:disabled{opacity:.4;cursor:not-allowed}

/* ---- storage-nag popup (owner joke feature, 2026-07-20) ---- */
.modal-card.stallone-modal{max-width:380px}
.modal-card.stallone-modal .stallone-img{display:block;width:100%;border:1px solid var(--hair);margin-bottom:10px}
.modal-card.stallone-modal .mbody p{margin:0 0 8px}
.modal-card.stallone-modal .mbody p:first-child{font-weight:700;letter-spacing:calc(.06em * var(--track));color:var(--bright)}
.modal-card.stallone-modal .mbody p:last-child{margin-bottom:0;color:var(--accent);font-style:italic}
/* the theatrical "Yes, Mr Stalone" checkout — a scripted terminal readout, no
   real network activity. See runStalloneCheckout (common.js). */
.nag-checkout{font-size:12px;line-height:1.9;color:var(--dim)}
.nag-checkout-line{opacity:0;animation:nagboxin .3s ease forwards}
.nag-checkout-final{margin-top:8px;font-weight:700;color:var(--bad)}
.nag-checkout-taunt{margin-top:6px;font-style:italic;color:var(--accent)}
@media(prefers-reduced-motion:reduce){.nag-checkout-line{animation:none;opacity:1}}
body.page-fadeout{transition:opacity .4s ease;opacity:0}
@media(prefers-reduced-motion:reduce){body.page-fadeout{transition:none}}

/* admin's variant add-form (/admin) — a plain row, NOT the uppercase .resultbar
   label styling its section header uses. */
.nag-addform{display:flex;gap:8px;flex-wrap:wrap;align-items:center;margin:10px 0}
.nag-addform input{flex:1;min-width:160px;font-size:13px}

/* the cutscene curtain (profile page): Stallone floats in and hovers, a text box
   fades in below him with the line, then the awarded title pops into the line,
   then the whole thing fades away to reveal the already-updated profile
   underneath (the gagtitle chip there gets its own pulse — see finish() in
   playNagCutscene, common.js). */
.nag-curtain{position:fixed;inset:0;z-index:var(--z-curtain);background:#000;
  display:flex;flex-direction:column;align-items:center;justify-content:center;gap:22px;
  transition:opacity .5s ease}
.nag-curtain.nag-out{opacity:0}
.nag-float{max-width:min(60vw,420px);max-height:50vh;opacity:0;
  animation:nagfloatin .7s cubic-bezier(.34,1.56,.64,1) both,
            nagbob 2.6s ease-in-out .7s infinite}
@keyframes nagfloatin{0%{transform:translateY(40px) scale(.9);opacity:0}100%{transform:translateY(0) scale(1);opacity:1}}
@keyframes nagbob{0%,100%{transform:translateY(0)}50%{transform:translateY(-14px)}}
.nag-textbox{max-width:min(80vw,420px);border:1px solid var(--accent);background:var(--panel);
  padding:12px 16px;font-size:13px;line-height:1.6;color:var(--bright);text-align:center;
  opacity:0;animation:nagboxin .5s ease both}
@keyframes nagboxin{from{opacity:0;transform:translateY(10px)}to{opacity:1;transform:translateY(0)}}
.nag-titlereveal{display:inline-block;font-weight:700;color:var(--accent);
  animation:nagtitlepop .5s cubic-bezier(.34,1.56,.64,1) both}
@keyframes nagtitlepop{0%{opacity:0;transform:scale(.5)}70%{opacity:1;transform:scale(1.2)}100%{transform:scale(1)}}
@media(prefers-reduced-motion:reduce){.nag-curtain{transition:none}
  .nag-float{animation:none;opacity:1}.nag-textbox{animation:none;opacity:1}.nag-titlereveal{animation:none}}

/* the COSMETIC gag_title chip (NOT a real crown — see prof-plaque). */
.prof-gagtitle{margin-top:9px;font-size:12px;letter-spacing:calc(.04em * var(--track));color:var(--accent);
  display:inline-flex;align-items:center;gap:5px;border:1px dashed var(--accent);
  border-radius:3px;padding:3px 8px;width:fit-content}
.prof-gagtitle.gagtitle-pulse{animation:nagchipglow .9s ease-out}
@keyframes nagchipglow{0%{box-shadow:0 0 0 0 rgba(237,167,51,.9)}100%{box-shadow:0 0 0 14px rgba(237,167,51,0)}}
@media(prefers-reduced-motion:reduce){.prof-gagtitle.gagtitle-pulse{animation:none}}

/* ═══ COINS (docs/SPEC_coins.md, owner ruling 2026-08-05) ═══════════════════════════════
   THE PURSE sits beside .scorechip and is deliberately its sibling in every way but colour:
   same height, same border, same tabular numerals, so the two read as one status group
   rather than a chip and a decoration. It carries its glyph as a real ELEMENT (.coinchip-i)
   rather than a ::before, because the balance can be negative and a pseudo-element cannot be
   restyled from the value — see .coinchip.debt below. */
/* ⚠ AND IT IS THE PURSE THAT CARRIES THE HIT-AREA SHAPE INTO THE CASINO. `header.cas-mast` builds its own bar and the purse is the ONE archive control on it, so these three lines (see .scorechip) are the whole fix there — .cas-door needs its own copy, in casino.css. */
.coinchip{--band:23px;display:inline-flex;align-items:center;gap:5px;margin-left:6px;position:relative;min-height:var(--tap-min);min-width:var(--tap-min);margin-block:calc((var(--band) - var(--tap-min)) / 2);
  padding:2px 8px;border:1px solid transparent;color:var(--gold, #d9a441);
  letter-spacing:calc(.08em * var(--track));font-size:11px;vertical-align:middle;text-decoration:none;cursor:pointer}
.coinchip:hover::after{border-color:var(--gold, #d9a441)}
/* ⛔ `.coinchip-i` HAS NO PAINT RULE ANY MORE and must not grow one back. It was a radial-gradient
   + inset box-shadow disc — a good picture ONLY this element could wear, which is exactly why the
   wallet and the casino floor were still typing ◉. The drawing is coinMark() (common-icons.js)
   now so every surface can hold it; the box comes from `.coinmark`, beside `.scoremark`. */

/* ⛔ THE `.wal-*` BLOCK IS GONE. 056444ce put /wallet on the panel primitive: page-wallet.js
   draws `.pn` panels and `.pns-wallet-*` (app-screens.css), and no file emitted a `.wal-*`
   class any more. Do not write one back here — the screen's paint lives in app-screens.css. */

/* GIFT MARKS — the paid badges on a post or comment. Sized to sit inline with a byline
   without changing its line box; the count only appears above one, because "◈" and "◈ 1" say
   the same thing and the second is noise. */
.giftmarks{display:inline-flex;align-items:center;gap:4px;margin-left:6px;vertical-align:middle}
.giftmark{display:inline-flex;align-items:center;gap:2px;font-size:11px;line-height:1}
.giftmark .gm-n{font-variant-numeric:tabular-nums;opacity:.8}

/* ═══ THE STONES — shard · fragment · gem (owner rulings 2026-08-06) ═══════════════════════
   GREY → WHITE → CYAN, ruled by the owner, and it is a VALUE ramp rather than three hues: one
   quiet, one bright, one the only chromatic thing in the row. It replaces accent(#eda733) →
   gold(#d9a441) → bright, whose first two rungs were the same colour to the eye at 13px, so
   the two cheapest tiers were separated by silhouette alone.

   ⚠ ONE PAIR OF TOKENS DRIVES EVERY SURFACE. The mark in a post's meta strip, the picker row,
   the gift button and the 46px cabinet stone all take their colour from `.gm-<kind>` through
   currentColor — so a re-ruling is these three lines, not a hunt. */
.gm-shard{color:#8d99ab}
.gm-fragment{color:var(--bright)}
.gm-gem{color:#45dbe4}
.gicon{width:1em;height:1em;display:block}                 /* follows the text size it sits in */
.giftmark .gicon{width:11px;height:11px}
.giftpick-opt .gicon{width:15px;height:15px}
.act .gicon{width:16px;height:16px}
/* The meta-strip mark (page-post.js metaStripFor): the strip sets 13px for its glyph slot and
   the stones honour it, so they line up with ▦ ◱ ◷ @ rather than floating above them. */
.metastrip .mi-gift .gicon{width:13px;height:13px}
.metastrip .mi-gift .mi-v{color:currentColor}

/* ═══ THE AWARD BAR (owner ruling 2026-08-06) ═════════════════════════════════════════════
   *"Awards need to render in their own bar on desktop as large and full colorful icons"* — its
   own surface under the media, not a 13px item filed between the file size and the OCR flag.
   The stones use the PLATE cut here (filled, faceted, lit); at 40px the flat mark would read as
   a sticker and the line-art detail as a diagram.

   ⚠ THE PHONE RULES LIVE HERE TOO, next to the desktop ones, even though the viewer's own sheet
   is app-viewer.css. One surface, one builder (giftBar), and its two sizes are four lines apart
   where a drift is visible — split across two files, "why is the phone one 40px" becomes an
   archaeology problem. */
.awardbar{display:flex;flex-wrap:wrap;align-items:center;gap:20px;
  border:1px solid var(--hair);background:var(--panel);padding:12px 16px;margin-top:10px}
.awardbar-i{display:inline-flex;align-items:center;gap:9px}
.awardbar-i .gicon{width:40px;height:40px;filter:drop-shadow(0 0 8px currentColor)}
.awardbar-n{font-size:17px;line-height:1;color:var(--bright);font-variant-numeric:tabular-nums}

/* THE PHONE VIEWER (owner ruling 2026-08-06, with a screenshot pointing at the empty right end
   of the poster row). Chromeless — no border, no panel — because it rides a row that is already
   a container, and `margin-left:auto` is what puts it where the arrow pointed. */
.awardbar-vw{border:none;background:none;padding:0;margin:0 0 0 auto;gap:12px;flex-wrap:nowrap}
.awardbar-vw .gicon{width:26px;height:26px;filter:drop-shadow(0 0 6px currentColor)}
.awardbar-vw .awardbar-n{font-size:13px}
/* A phone narrow enough that three stones would push the ⋯ off the row drops the counts before
   it drops the stones — which award it is beats how many, at a glance, on a 360px screen. */
@media (max-width:400px){
  .awardbar-vw{gap:8px}
  .awardbar-vw .gicon{width:22px;height:22px}
  .awardbar-vw .awardbar-n{font-size:11px}
}

/* THE CABINET (profStones, common-profile.js) — the ONE surface with room for the detail cut. */
.stone-wall{display:flex;flex-wrap:wrap;gap:12px}
.stone-box{width:118px;padding:16px 10px 12px;text-align:center;border:1px solid currentColor;
  border-radius:5px;background:var(--panel);cursor:default;display:flex;flex-direction:column;
  align-items:center;gap:8px}
.stone-i{display:flex;align-items:center;justify-content:center;height:46px}
.stone-i .gicon{width:46px;height:46px;filter:drop-shadow(0 0 7px currentColor)}
.stone-n{font-size:20px;line-height:1;font-variant-numeric:tabular-nums;color:currentColor}
.stone-l{font-size:11px;letter-spacing:calc(.05em * var(--track));color:var(--bright)}
/* Only the gem glows on its own — it is the one stone that emits (its corona), and a shelf
   where all three shimmer is a shelf with no ladder left in it. */
.stone-box.gm-gem{box-shadow:0 0 16px rgba(69,219,228,.16);background:linear-gradient(180deg,rgba(69,219,228,.05),transparent)}

/* THE GIFT PICKER — an inline disclosure under the action bar, not a modal, matching the
   other reveal controls on the post page. */
.giftpick{border:1px solid var(--hair-hi);padding:10px;margin-top:8px;display:flex;flex-direction:column;gap:6px}
.giftpick-h{font-size:11px;letter-spacing:calc(.1em * var(--track));opacity:.7}
.giftpick-opt{display:grid;grid-template-columns:auto 1fr auto auto;gap:10px;align-items:baseline;
  background:none;border:1px solid var(--hair);color:inherit;padding:7px 9px;cursor:pointer;text-align:left;font:inherit}
.giftpick-opt:hover:not(:disabled){border-color:var(--accent)}
.giftpick-opt:disabled{opacity:.5;cursor:default}
.giftpick-price{font-variant-numeric:tabular-nums;color:var(--gold, #d9a441)}
.giftpick-net{font-size:11px}
.giftpick-msg:empty{display:none}

/* THE CASINO. Deliberately spare: a balance, a wager, two calls, and the result. A slot-machine
   of decoration around real coins somebody earned curating would be the wrong register. */
.cas-head{padding:18px 0 12px;border-bottom:1px solid var(--hair);margin-bottom:14px}
.cas-title{font-size:12px;letter-spacing:calc(.18em * var(--track));opacity:.75;text-transform:uppercase}
.cas-balance{font-size:30px;line-height:1.15;color:var(--bright);font-variant-numeric:tabular-nums;margin-top:4px}
.cas-coin{color:var(--gold, #d9a441)}
.cas-terms{margin-top:6px;opacity:.75}
.cas-form{display:flex;align-items:center;gap:10px;margin-bottom:12px}
.cas-lbl{font-size:11px;letter-spacing:calc(.1em * var(--track));opacity:.7}
.cas-wager{background:var(--bg-2, #111);border:1px solid var(--hair-hi);color:var(--bright);
  padding:6px 8px;width:9ch;font:inherit;font-variant-numeric:tabular-nums}
.cas-calls{display:flex;gap:10px}
.cas-call{flex:1;background:none;border:1px solid var(--hair-hi);color:inherit;font:inherit;
  padding:14px 0;cursor:pointer;letter-spacing:calc(.12em * var(--track));text-transform:uppercase;font-size:12px}
.cas-call:hover:not(:disabled){border-color:var(--gold, #d9a441);color:var(--bright)}
.cas-call:disabled{opacity:.45;cursor:default}
.cas-result{min-height:2.2em;margin-top:16px;font-size:22px;letter-spacing:calc(.1em * var(--track));
  text-align:center;text-transform:uppercase}
.cas-result.won{color:var(--good, #6fbf73)}
.cas-result.lost{color:var(--dim)}
.cas-msg{text-align:center;min-height:1.6em}  /* reserves the FILLED line, not the empty one */
.cas-foot{margin-top:22px;text-align:center;opacity:.6}

/* ═══ THE SIGILS — THE MARKS BEFORE A NAME, AND THE MARK ON A GATED ACTION ═════════════════
   docs/SPEC_sigils.md (owner ruling 2026-09-01). The drawings are web/sigils.js and are reached
   through `sigilSlots()` / `permitMark()`; ⛔ nothing here draws one.

   ⚖ THEY LIVE IN app-rank.css BECAUSE THEY LIVE EVERYWHERE. This sheet already holds the other
   two identity treatments a username wears — the apex crown and the two class colours — and it
   is linked by every page in the archive. clubs.css is linked by ONE, and a mark that only has
   a box on /clubs is a mark that draws at 100% width the first time a name is printed anywhere
   else (measured once already, on the roster, where it pushed the list off the screen).

   ⚠ THE SIZE IS DECLARED ON THE BOX AND ON THE <svg>. `sigilMark()` builds an unstyled node with
   a viewBox and no width, and a viewBox with no box is 100% WIDE.
   ⚠ 14px AT A 12px NAME is the sketch's own proportion — the mark reads at the line's cap height
   rather than at its em box, which is why it is larger than the text and still does not tower. */
.sg{display:inline-flex;flex:none;width:14px;height:14px;font-style:normal;vertical-align:-2px;margin-right:3px}
.sg svg{display:block;width:14px;height:14px;flex:none}
/* the last mark in a prefix owns the gap to the name; the pip/crown after it brings its own. */
.ulink .sg,.prof-name .sg{margin-right:.24em}

/* ⚑ TOKENS, NEVER LITERAL HEXES — app-skins.css is the law, and these are the same six hues the
   club roster's badges already wear (clubs.css "ROLE MARKERS"), so the two sets cannot drift
   apart while both are on the screen. ⚖ SCION KEEPS GREEN AND BINKIE KEEPS PINK (SPEC_scion.md
   v2 §3.6): the class colour is the account's, and a sigil that recoloured it would be a second
   answer to "what colour is a scion". */
.sg-founder{color:var(--bright)}                                        /* the shred mark */
.sg-admin  {color:var(--bad)}                                           /* the bolt — red */
.sg-mod    {color:color-mix(in srgb,var(--bad) 45%,var(--accent-deep))} /* orange */
.sg-scion  {color:var(--scion)}                                         /* green — the class's own token */
.sg-binkie {color:var(--binkie)}                                        /* pink — the class's own token */
.sg-owner  {color:var(--accent)}                                        /* amber — the club crown */
/* ⚠ STAFF AND NEW ARE QUIETER BY RULING (§3), and quieter means LOWER CONTRAST, not smaller: a
   shrunken glyph in a row of full-size ones reads as a rendering fault. Both still clear 4.5:1
   on --void, which is the repo floor and is not negotiable for a mark that makes a claim about
   a person. */
.sg-staff  {color:var(--info)}                                          /* cyan */
.sg-new    {color:var(--dim)}                                           /* the quietest — a time, not an authority */

/* THE GATED-ACTION MARK (§2) — a menu row, a button, a sheet action. It leads the row, in front
   of the row's own verb glyph: the verb says what the control does, the sigil says which power
   it is. ⛔ Never the viewer's own top mark on every row. */
.sg-permit{margin-right:6px}

/* ⚖ SLOT 2 IS MARKED ON THE ELEMENT so a reader — and a gate — can tell a club mark from a site
   mark on the page: "the same name in the home feed shows no club mark" is provable only if the
   club slot is nameable, and `.sg-clubslot` is that name. It carries no paint — the WORD it wears
   (.sg-owner / .sg-staff / .sg-new) already does. */
