/* rip-header.css — the /rip/ header.
 *
 * Pairs with rip-header.js. Built on the rip-ui tokens so the header is
 * identical on the pack app and on every member page — that is the whole
 * point of it existing.
 *
 * The height is reserved on <body> before the JS runs, so injecting the bar
 * cannot shift the page underneath it.
 */

:root{ --rh-h:60px; }
@media(min-width:900px){ :root{ --rh-h:66px; } }

/* ⚠ THE CHROME OWNS ITS OWN BOX MODEL. THERE IS NO SITE-WIDE RESET.
   Measured at 390px signed in: the open burger menu on /rip/leaderboard.php
   painted to x=412 against a 390px viewport (+22px past the edge) and the
   Trades count pill sat at right=393, sliced by the screen edge — the exact
   screenshot Aaron sent. Every other page was fine at 374.

   The difference was never the menu. `.rh-menu-link` is width:100% with
   `padding:0 18px` and a 1px border, which is 38px over its container UNLESS
   box-sizing is border-box — and border-box was arriving by luck, from
   whatever `*{box-sizing:border-box}` the HOST page happened to declare
   (account.css:48, home.css:30, and the resets on packs/collectors/binder).
   leaderboard.css declares none, so the shared header inherited content-box
   there and only there. That is a fault that reappears the moment somebody
   adds a page without a reset, which is why it is fixed here rather than by
   adding a fourteenth copy of the reset to leaderboard.css.

   Scoped to the header's own subtree, not `*`, so injecting the chrome can
   never change the box model of the page it is injected into — the same
   containment tier-bar.css:69 and trades.css:34 already use for the same
   reason. */
.rh,.rh *,.rh *::before,.rh *::after,
.rh-menu,.rh-menu *,.rh-menu *::before,.rh-menu *::after{ box-sizing:border-box; }

body{ padding-top:var(--rh-h); }
/* ⚠ EVERY IN-PAGE SCROLL LANDS UNDER THE FIXED BAR WITHOUT THIS. The header is
   position:fixed, so an anchor jump or a scrollIntoView({block:'start'}) puts
   the target's own top at viewport 0 — which is 60px (66 on desktop) BEHIND the
   bar. Live consequences before this rule, all measured at 390px on
   /rip/packs: the skip link, the one control a keyboard or screen-reader user
   has, landed on the h1's SECOND line; every pull tucked the top 60px of the
   slab reveal under the bar; same for the leaderboard jump, the badge filter
   chips, and account.php's "Add social handle" anchor.

   scroll-padding on the scrolling element is the right lever, not scroll-margin
   on each target: it fixes hash navigation AND every scrollIntoView call site
   at once, needs no per-target rules and no JS, and reads the same --rh-h token
   so it tracks the 60->66px breakpoint by itself. The +8 matches the breathing
   room getScrollOffset() uses on the main site.

   Side effect, and it is a correction too: the few block:'center' calls in
   packs.htm now centre within the padded scrollport, so they sit ~34px lower.
   They were previously centring under a fixed bar. */
html{ scroll-padding-top:calc(var(--rh-h) + 8px); }
html.rh-open{ overflow:hidden; }          /* mobile menu owns the viewport */

.rh{
    position:fixed; top:0; left:0; right:0; z-index:9000;
    height:var(--rh-h);
    background:rgba(10,14,21,.86);
    border-bottom:1px solid var(--rip-line);
    -webkit-backdrop-filter:saturate(140%) blur(14px);
    backdrop-filter:saturate(140%) blur(14px);
}
.rh-in{
    max-width:1120px; margin:0 auto; height:100%;
    padding:0 16px; display:flex; align-items:center; gap:18px;
}

/* ── brand ─────────────────────────────────────────────────────────────── */
.rh-brand{
    display:flex; align-items:center; gap:9px; flex:0 0 auto; min-width:0;
    text-decoration:none; color:var(--rip-fg);
}
/* ⚠ NO CIRCULAR FRAME. These rules dressed a PHOTO — a round crop with a hairline
   border, which is right for an avatar and wrong for a mark. The Pack Dojo logo is
   an ensō: a ring inside a bordered circle reads as two rings fighting, which is
   exactly how it rendered the first time. The SVG carries its own silhouette, so
   the frame comes off and the box just holds it. */
.rh-logo{ width:30px; height:30px; display:block; flex:0 0 auto; }
/* Never hidden. Hiding it below 520px left the phone header as an avatar and a
   burger with nothing between them, and a stranger could not tell what the site
   was without scrolling. The name IS the header's job. */
.rh-brand-tx{ display:flex; flex-direction:column; min-width:0; line-height:1.05; }
.rh-brand-txt{
    display:block;
    /* 17 -> 20px: "a little bigger font". The row can afford it now that the
       strapline is gone. */
    font-family:var(--rip-display); font-weight:800; font-size:20px;
    letter-spacing:-.02em; white-space:nowrap;
}
/* ⚠ .rh-brand-sub NO LONGER EXISTS. The "FREE PACK SIMULATOR" strapline was
   removed on 2026-08-08 at Aaron's request. Do not reinstate it without
   re-measuring the row: it was nineteen letter-spaced characters, WIDER on a
   phone than the name above it, and it was the reason the counter strip had to
   drop one of its three figures below 560px. Its width is now paying for the
   larger name and for all three counters. */
@media(max-width:460px){
    .rh-in{ gap:8px; padding:0 12px }
}

/* ── desktop links ─────────────────────────────────────────────────────── */
.rh-nav{ display:none; align-items:center; gap:4px; margin-left:6px; }
/* ⚠ 1060, NOT 900 — THE INLINE NAV APPEARS ONLY WHEN THE WHOLE ROW FITS WITH
   ALL THREE COUNTERS (2026-08-30). At 900 it did not, and the counters were
   what gave way: measured signed-in on /rip/packs, the row needs
   brand 131 + nav 572 + stats 118 + account 124 + 32 padding + 54 gaps =
   1031px, and .rh-in is capped at the site's 1120px column. From 900 to 1030
   there is simply not enough width for both, so the bar showed the links and
   ATE the counters — at 900 all three were clipped to nothing, and Aaron got a
   header with no members, no ripping and no pulls-left at all.
   Below this the burger carries the same links, one tap away; above it there
   is 89px of slack. Re-measure this number if a nav item is ever added. */
@media(min-width:1060px){ .rh-nav{ display:flex } }

.rh-link{
    display:inline-flex; align-items:center; height:38px; padding:0 14px;
    border-radius:9px; text-decoration:none;
    font-family:var(--rip-body); font-size:14.5px; font-weight:700;
    color:var(--rip-fg-2); white-space:nowrap;
    transition:color .15s, background .15s;
}
.rh-link:hover{ color:var(--rip-fg); background:var(--rip-panel); }
.rh-link.is-on{ color:var(--rip-fg); background:var(--rip-panel-2); }
/* The one thing you click. Everything else in the bar is navigation. */
.rh-link--cta{
    background:linear-gradient(135deg,var(--rip-act),var(--rip-act-2));
    color:var(--rip-act-ink); font-weight:800;
    box-shadow:0 4px 16px rgba(46,230,214,.22);
}
.rh-link--cta:hover,.rh-link--cta.is-on{
    background:linear-gradient(135deg,var(--rip-act),var(--rip-act-2));
    color:var(--rip-act-ink); box-shadow:0 7px 22px rgba(46,230,214,.32);
}

/* ── live counters ─────────────────────────────────────────────────────────
   Lifted out of a second bar on the pack app. They sit hard against the right
   side of the bar, before the account controls, and they are allowed to
   disappear entirely rather than squeeze the brand: below 430px there is no
   room for both the product name and three figures, and the name wins. */
.rh-stats{
    margin-left:auto; display:flex; align-items:center; gap:12px;
    flex:0 1 auto; min-width:0; overflow:hidden;
}
/* min-width:0 so a stat can be dropped by the strip's overflow rather than
   forcing the bar wider than the viewport. */
.rh-stat{
    display:inline-flex; align-items:baseline; gap:5px; white-space:nowrap;
    flex:0 0 auto; min-width:0;
    font-family:var(--rip-body); font-size:11px; color:var(--rip-fg-3);
}
.rh-stat b{
    font-family:var(--rip-data); font-weight:800; font-size:13.5px;
    color:var(--rip-fg); font-variant-numeric:tabular-nums;
}
.rh-stat-i{ flex:0 0 auto; align-self:center; color:var(--rip-fg-3) }
/* ⚠ THE WORDS ARE VISUALLY HIDDEN AT EVERY WIDTH, AND THAT IS FORCED, NOT
   PREFERRED (2026-08-30). "members" + "ripping" + "left" cost 130px, and the
   row needs 1161px with them against a 1120px column — so on DESKTOP they do
   not fit at any viewport, because .rh-in is capped at the site-wide 1120 that
   .rf-in and .rh-menu-in also use. Widening the header alone would take the
   brand out of line with the footer and the page under it for the sake of
   three words. What the bar is for is the figures, so the figures stay.
   They used to be display:none below 660px, which took them out of the
   accessibility tree as well — and #rh-stats is an aria-live region, so a
   screen reader heard "553" with nothing to say what 553 was. Clipped rather
   than removed: sighted users get icon + number, assistive tech still gets the
   word. Do not "simplify" this back to display:none. */
.rh-stat-l{
    position:absolute; width:1px; height:1px; padding:0; margin:-1px;
    overflow:hidden; clip:rect(0 0 0 0); clip-path:inset(50%);
    white-space:nowrap; border:0;
}
.rh-stat--live b{ color:var(--rip-up) }
.rh-stat-dot{
    width:7px; height:7px; border-radius:50%; background:var(--rip-up);
    box-shadow:0 0 6px var(--rip-up); align-self:center;
    animation:rh-pulse 1.8s ease-in-out infinite;
}
@keyframes rh-pulse{ 0%,100%{opacity:1} 50%{opacity:.35} }
/* Amber is the money colour everywhere else in rip-ui, and a pull allowance is
   the closest thing this app has to a balance. Red once it is spent. */
.rh-stat--pulls b{ color:var(--rip-val) }
.rh-stat--pulls.is-out b{ color:var(--rip-down) }
/* Shed width in the order the figures stop needing it: the words first, then
   the gaps, then the whole strip. Aaron's phone is 390px and the counters must
   survive there — that is the width the request was made from — so the strip
   only disappears on genuinely tiny screens. */
@media(max-width:660px){ .rh-stats{ gap:11px } }
/* ⚠ THE SIGNED-OUT COUNTER DROP IS GONE. Aaron, 2026-08-08: "make sure all 3
   pills on the right are showing next to the menu." It existed because the
   brand block carried a two-line name plus a 149px strapline, and that plus a
   "Sign in" pill plus three counters plus the burger did not fit — the strip
   was clipping a number in half rather than admitting it. The strapline is now
   gone and the row has the width back, verified at 320-1920. */
@media(max-width:420px){ .rh-stats{ gap:9px } .rh-stat b{ font-size:13px } }
/* ⚠ BELOW 390 THE STRIP DROPS A FIGURE RATHER THAN CLIPPING ONE (2026-08-30).
   `overflow:hidden` on this strip does NOT drop items — its children are
   flex:0 0 auto, so what it actually does is slice the last number in half, and
   that is what it was doing: measured signed-out, "3 ripping" was cut by 6px at
   375, 21px at 360 and 37px at 344, and signed-in "47" lost 5px at 320.
   Half a number is worse than no number, because it still reads as a number.
   "Ripping now" is the one that goes first: members is the product's headline
   figure and pulls-left is the reader's own allowance, while this one is about
   other people. It is also already self-hiding at zero, so it is the figure the
   bar is most used to being without.

   ⚠ AND THEN THE LABELS WENT, AND NOBODY RE-MEASURED THIS. Aaron, 2026-08-31:
   "I can't see how many people are pulling right now. did you remove it?" No —
   `@media(max-width:389px){.rh-stat--live{display:none}}` was hiding it, and
   that cutoff was measured while the words "members / ripping / left" were
   still painted. Clipping those freed ~130px and made the rule obsolete the
   same day, but it stayed. His phone is under 390 CSS px, so he had not seen
   the figure since.

   Re-measured on the real page, signed in and signed out, at 390/375/360/349/
   344/330/320/310/300/280 — and at the worst numbers the site can produce
   (12,480 members · 120 ripping · 70 left, i.e. members five digits and ripping
   three): the strip does not overflow at ANY of them with all three showing.
   So the rule is gone. Below 320 the other rules already drop members, and
   below 300 the whole strip; those are still measured and still stand.
   ⚠ SIGNED OUT IS 365, NOT NOTHING — THE "Join free" PILL IS THE DIFFERENCE.
   That row carries a ~90px pill the signed-in row does not, the same 381-vs-325
   budget the note below already turns on. Measured at the worst numbers: it
   fits at 366 and slices at 364, so the live figure goes below 365 signed out
   and at no width at all signed in. Re-measure BOTH rows if anything enters or
   leaves either one.
   `rip/admin/cli/header-stats-check.js` asserts it now. */
@media(max-width:365px){ .rh--out .rh-stat--live{ display:none } }
/* Signed out the row also carries the 90px "Join free" pill, which the signed-in
   row does not — that one pill is the whole difference between the two budgets
   (381px signed out vs 325 signed in). Below 350 there is no arrangement that
   fits it, the brand and any figure at all, and the brand wins: a stranger who
   cannot tell what the site is has no reason to care how many members it has. */
@media(max-width:349px){
    .rh--out .rh-stats{ display:none }
    .rh--out .rh-right{ margin-left:auto }
}
/* The strip used to vanish below 340px because the brand block carried a
   149px strapline as well as the name. With that gone the counters fit further
   down; re-measured after the change and 320px is clean, so the cutoff moves to
   the narrowest viewport anyone actually has. Below 300 the bar is a lost cause
   and the counters are the right thing to drop. */
/* ⚠ 300-319 IS A REAL WIDTH (Galaxy Fold cover) AND IT ONLY BREAKS ONCE THE
   MEMBER COUNT REACHES FIVE DIGITS. At 553 members it fits today; at 12,480 the
   members figure is wide enough to push pulls-left 15px out of the strip. The
   figure that goes is members, because it is the one that GROWS — and because
   pulls-left is the reader's own allowance, which is the only figure here they
   can act on. The brand drops back to the 17px it was before the strapline came
   out, which is also what stops the signed-out row overflowing by 11px at 280.
   ⚠ pulls-left is at most TWO digits and that is computed, not assumed:
   SIM_PULLS_DAILY_CAP 50 + SIM_REFERRAL_PULLS_MAX 20 = 70. An earlier pass here
   was sized for "150" and fixed a width that cannot occur. */
@media(max-width:319px){
    .rh-brand-txt{ font-size:17px }
    .rh-stat:not(.rh-stat--live):not(.rh-stat--pulls){ display:none }
}
@media(max-width:299px){
    .rh-stats{ display:none }
    .rh-right{ margin-left:auto }     /* the strip was carrying the push */
}

@media(prefers-reduced-motion:reduce){ .rh-stat-dot{ animation:none } }

/* ── right side ────────────────────────────────────────────────────────── */
.rh-right{ display:flex; align-items:center; gap:8px; flex:0 0 auto; }

.rh-acct,.rh-signin{
    display:inline-flex; align-items:center; gap:8px; height:40px; padding:0 15px;
    border-radius:999px; cursor:pointer; text-decoration:none;
    background:var(--rip-panel-2); border:1px solid var(--rip-line-2); color:var(--rip-fg);
    font-family:var(--rip-body); font-size:14px; font-weight:700; white-space:nowrap;
    transition:border-color .15s, color .15s;
}
.rh-acct:hover,.rh-signin:hover{ border-color:var(--rip-act-line); color:var(--rip-act); }
/* ⚠ THIS DISC NOW MEANS SOMETHING. It used to paint on every signed-in load,
   which is a dot that says only "you are signed in" — a fact the button next to
   it already states in words. It is the desktop half of the waiting marker now,
   because .rh-burger is display:none from 900px (line 186) and the pill it
   would otherwise point at lives inside the menu panel. Same class, same 8px,
   same token, as instructed — do not invent a second dot for the account
   button. display:none rather than opacity so it takes no room in the button's
   flex row while nothing is waiting. */
.rh-acct-dot{ display:none; width:8px; height:8px; border-radius:50%; background:var(--rip-act); flex:0 0 auto; }
.rh-acct.rh-has-waiting .rh-acct-dot{ display:block; }
.rh-caret{ font-size:10px; color:var(--rip-fg-3); }
@media(max-width:899px){ .rh-acct{ display:none } }

/* position:relative for the waiting disc below — the burger is the only thing
   on screen below 900px that can carry it, since the pill is inside the panel
   the burger opens. */
.rh-burger{
    position:relative;
    display:inline-flex; flex-direction:column; justify-content:center; gap:5px;
    width:42px; height:42px; padding:0 10px; cursor:pointer;
    background:var(--rip-panel-2); border:1px solid var(--rip-line-2); border-radius:10px;
}
/* ⚠ DRAWN, NEVER A GLYPH. A colour emoji is ~1.25em and the strip's width budget
   at 390px is already spent — that is what pushed this very burger off Aaron's
   phone when the counters shipped with 👥. It is also untestable from here: this
   container has no colour-emoji font, so a headless screenshot renders a narrow
   mono fallback and looks fine. A pseudo-element is 7px on every device.
   The ring is the page ground, not the bar's own rgba(10,14,21,.86): it has to
   read as a gap over the burger's border AND over the 2px-thick top bar of the
   X the burger becomes while the menu is open. */
.rh-burger.rh-has-waiting::after{
    content:''; position:absolute; top:-3px; right:-3px;
    width:7px; height:7px; border-radius:50%;
    background:var(--rip-act); box-shadow:0 0 0 2px var(--rip-bg);
}
.rh-burger span{ display:block; height:2px; background:var(--rip-fg); border-radius:2px; transition:transform .18s, opacity .18s; }
html.rh-open .rh-burger span:nth-child(1){ transform:translateY(7px) rotate(45deg) }
html.rh-open .rh-burger span:nth-child(2){ opacity:0 }
html.rh-open .rh-burger span:nth-child(3){ transform:translateY(-7px) rotate(-45deg) }
/* ⚠ MOVES WITH .rh-nav, ALWAYS. These two are one switch: the burger is the
   only route to the secondary links while the inline nav is hidden, so if this
   number and the nav's ever disagree, the band between them has NEITHER and
   half the site becomes unreachable. */
/* ⚠ THE BURGER IS GONE ENTIRELY (Aaron, 2026-08-31: "I didn't realise every
   link would be on there, that's great. that means yes, you can remove the
   burger menu on mobile") — and since it was ALREADY hidden from 1060px up,
   "on mobile" means at every width. The tab bar carries all twelve
   destinations now, four as tabs and eight in the Me sheet, so a second way in
   is the thing nobody opened.
   ⚠ THE THREE THINGS THE MENU CARRIED THAT THE TABS DID NOT — the signed-in
   email, Admin and Log out — MOVED INTO THE SHEET. Hiding the burger without
   moving them would have taken Log out off every phone. The menu element still
   exists and is still reachable from "My Account" at ≥900px; only its button
   at the small end is gone. */
.rh-burger{ display:none }
/* The waiting disc has one home at a time. The account pill appears at 900 and
   carries it from there (.rh-acct-dot), but the burger now survives to 1059 —
   so between 900 and 1059 both were on screen and the same notification was
   painted twice. The pill wins in that band because it is the thing the marker
   is pointing at. */
@media(min-width:900px){ .rh-burger.rh-has-waiting::after{ display:none } }

/* ── the menu ──────────────────────────────────────────────────────────────
   Full-screen on a phone with big, readable text — a cramped dropdown on a
   small screen is how you end up mis-tapping. A normal dropdown on desktop. */
.rh-menu{
    position:fixed; top:var(--rh-h); left:0; right:0; bottom:0; z-index:8999;
    background:rgba(7,10,16,.97);
    -webkit-backdrop-filter:blur(18px); backdrop-filter:blur(18px);
    overflow-y:auto; -webkit-overflow-scrolling:touch;
}
.rh-menu[hidden]{ display:none }
.rh-menu-in{ max-width:1120px; margin:0 auto; padding:22px 16px 40px; display:flex; flex-direction:column; gap:6px; }

.rh-who{
    font-family:var(--rip-body); font-size:13px; color:var(--rip-fg-3);
    margin:0 0 12px; padding:0 4px; overflow-wrap:anywhere;
}
.rh-who b{ color:var(--rip-fg-2); font-weight:700 }
.rh-menu-group{ display:flex; flex-direction:column; gap:6px; }

.rh-menu-link{
    display:flex; align-items:center; width:100%; min-height:56px; padding:0 18px;
    border-radius:12px; text-decoration:none; text-align:left; cursor:pointer;
    background:var(--rip-panel); border:1px solid var(--rip-line);
    color:var(--rip-fg);
    font-family:var(--rip-display); font-size:19px; font-weight:800; letter-spacing:-.018em;
    transition:border-color .15s, background .15s, color .15s;
}
.rh-menu-link:hover{ background:var(--rip-panel-2); border-color:var(--rip-line-2); }
.rh-menu-link.is-on{ border-color:var(--rip-act-line); color:var(--rip-act); background:var(--rip-act-soft); }
.rh-menu-link--admin{ color:var(--rip-val); border-color:rgba(242,177,52,.35); }
/* Log out is the only destructive thing in here, and it reads that way.
   The LABEL is --rip-down, not --rip-danger: #e11d48 measures 3.83:1 on the
   panel it sits on and 3.49:1 on the hover panel-2, and at 17px/800 (14px on
   desktop, line 246) this is normal text, so the floor is 4.5:1. --rip-down
   #fb7185 is 6.68:1 / 6.09:1 and is already this bar's negative-red text token
   (.rh-stat--pulls.is-out b, line 137). The border keeps --rip-danger — it
   carries the warning without having to be legible. */
.rh-menu-link--out{ margin-top:10px; color:var(--rip-down); border-color:rgba(225,29,72,.35); font-size:17px; }

/* The waiting count, on the Trades & Gifts entry. Cyan because cyan is action
   here and this is the one row in the panel that wants a decision; amber is
   money and would read as a price.
   ⚠ THE [hidden] RULE IS LOAD-BEARING, NOT TIDINESS. The UA sheet's
   `[hidden]{display:none}` is a plain (0,0,1) element-attribute rule and loses
   to this class's display:inline-flex, so without it the pill paints an empty
   cyan lozenge on every member's menu when nothing is waiting — the exact shape
   of bug that looks like a broken count rather than a CSS ordering mistake.
   ⚠ Digits line up with font-variant-numeric, never with a mono face: there is
   no monospaced family on this site and --rip-data is Bricolage. 700 is inside
   Bricolage's 600-800 axis; 900 here would be a synthesised face on Android. */
.rh-count{
    margin-left:auto; flex:0 0 auto;
    display:inline-flex; align-items:center; justify-content:center;
    min-width:24px; height:24px; padding:0 7px; border-radius:999px;
    background:var(--rip-act); color:var(--rip-act-ink);
    font-family:var(--rip-data); font-size:13px; font-weight:700; line-height:1;
    font-variant-numeric:tabular-nums; letter-spacing:0;
}
.rh-count[hidden]{ display:none; }

/* STAYS position:fixed on desktop. It was `absolute` and the panel is a SIBLING
   of <header> at body level with nothing positioned above it, so its containing
   block was the initial containing block — DOCUMENT coordinates. `top:60px` meant
   60px from the top of the page, not the top of the screen: open the account menu
   anywhere but at scrollY 0 and the button reported aria-expanded="true" while the
   panel painted a full screenful above the viewport. Measured at 1280x820 signed
   in, scrolled to 1073: menu top -1013, Admin -500, Log out -443, nothing on
   screen. Admin and Log out live nowhere else in the chrome, so both were
   unreachable until you scrolled back to the very top. Mobile was never affected —
   it keeps the base rule's `fixed`.
   Fixed does not make the panel drift while scrolling either: html.rh-open sets
   overflow:hidden above, so the page is scroll-locked on desktop too.
   max-height + overflow-y: at 1280x600 the panel is 625px tall against 600px of
   viewport, so Log out sat below the fold with the page locked and the panel
   clipped by overflow:hidden — no way to reach it at ANY scroll position. It now
   scrolls itself. overflow-x stays hidden so the border-radius still clips. */
@media(min-width:900px){
    .rh-menu{ position:fixed; top:calc(var(--rh-h) - 6px); left:auto; right:max(16px,calc(50% - 560px)); bottom:auto;
              width:280px; border:1px solid var(--rip-line-2); border-radius:var(--rip-r);
              background:var(--rip-panel); box-shadow:var(--rip-shadow);
              max-height:calc(100vh - var(--rh-h) - 24px); overflow-x:hidden; overflow-y:auto; }
    .rh-menu-in{ padding:12px; gap:3px }
    .rh-menu-group{ gap:3px }
    .rh-menu-link{ min-height:42px; font-size:14.5px; font-family:var(--rip-body); font-weight:700;
                   border-color:transparent; background:transparent; padding:0 12px; }
    .rh-menu-link--out{ font-size:14px }
    /* The desktop row is 42px, not 56 — a 24px pill inside it leaves 9px of
       clearance and reads as a button. */
    .rh-count{ min-width:20px; height:20px; padding:0 6px; font-size:12px; }
}

/* ── the header owns its own colours ───────────────────────────────────────
   rip-ui.css paints every anchor with `.rip-ui a:not(.rip-btn){color:act}`.
   That selector is specificity (0,2,1) — it outranks a plain .rh-link class
   (0,1,0), so on any page whose <body> carries .rip-ui (the welcome page, the
   preview hub) the whole bar came out cyan: the brand, the inactive links,
   and worst of all the CTA's label, which is cyan text on a cyan fill and
   therefore invisible.

   Every selector below is (0,3,0) or better, so it wins outright rather than
   winning on source order — which would silently flip the day someone
   reorders two <link> tags. */
.rh a.rh-brand:not(.rip-btn){ color:var(--rip-fg); }
.rh a.rh-link:not(.rip-btn){ color:var(--rip-fg-2); }
.rh a.rh-link:not(.rip-btn):hover,
.rh a.rh-link.is-on:not(.rip-btn){ color:var(--rip-fg); }
.rh a.rh-link--cta:not(.rip-btn),
.rh a.rh-link--cta:not(.rip-btn):hover{ color:var(--rip-act-ink); }
.rh a.rh-signin:not(.rip-btn){ color:var(--rip-fg); }
.rh a.rh-signin:not(.rip-btn):hover{ color:var(--rip-act); }
.rh-menu a.rh-menu-link:not(.rip-btn){ color:var(--rip-fg); }
.rh-menu a.rh-menu-link.is-on:not(.rip-btn){ color:var(--rip-act); }
.rh-menu a.rh-menu-link--admin:not(.rip-btn){ color:var(--rip-val); }
/* And no underline on hover — rip-ui adds one to every anchor. */
.rh a:not(.rip-btn):hover,.rh-menu a:not(.rip-btn):hover{ text-decoration:none; }

.rh a:focus-visible,.rh button:focus-visible,.rh-menu a:focus-visible,.rh-menu button:focus-visible{
    outline:2px solid var(--rip-act); outline-offset:2px;
}
@media(prefers-reduced-motion:reduce){
    .rh-link,.rh-acct,.rh-signin,.rh-menu-link,.rh-burger span{ transition:none }
}

/* ══════════════════════════════════════════════════════════════════════════
   THE TAB BAR (.rtb) — Aaron, 2026-08-31: "nobody looks in the burger menu…
   I'm considering some buttons at the bottom of every page, made to look like
   an app". He is right about the burger: it is one tap that costs a decision,
   and every measurement of a hidden menu says the same thing.

   ⚠ IT LIVES HERE, NOT IN ITS OWN FILE, AND THAT IS THE WHOLE DEPLOY STORY.
   These bytes reach exactly the pages that already link rip-header.css — the
   13 hosts Aaron picked — with no new <link>, no new ?v= to hand-type on the
   three static pages, and no chance of a host getting the JS and not the CSS.

   ⚠ MOBILE ONLY, AT THE SAME 1060px THE INLINE NAV USES. Above that the bar
   already shows the primary links in .nav-bottom-row, so a second permanent
   nav pinned to the bottom of a desktop window is chrome with nothing to do.
   One breakpoint, shared: if these two ever disagree there is a band with two
   navs or none, which is the exact failure recorded for .rh-nav/.rh-burger.
   ══════════════════════════════════════════════════════════════════════════ */
.rtb{
    position:fixed; left:0; right:0; bottom:0; z-index:8500;
    display:flex; box-sizing:border-box;
    background:var(--rip-panel); border-top:1px solid var(--rip-line-2);
    padding:5px 3px calc(5px + env(safe-area-inset-bottom,0px));
    /* the ground is opaque: this sits over a page that scrolls under it */
    -webkit-backdrop-filter:none; backdrop-filter:none;
}
@media(min-width:1060px){ .rtb{ display:none } }

.rtb-tab{
    flex:1 1 0; min-width:0; position:relative;
    display:flex; flex-direction:column; align-items:center; gap:3px;
    padding:6px 2px; text-decoration:none; cursor:pointer;
    background:none; border:0; font-family:var(--rip-body);
    color:var(--rip-fg-3); transition:color .15s;
}
.rtb-tab svg{ width:22px; height:22px; display:block; flex:0 0 auto }
/* ⚠ 10.5px AND ONE WORD. At 390px a tab is 78px wide; a second word wraps and
   the bar grows a row. Rename a tab only after measuring it at 320px. */
.rtb-tab span{
    font-size:10.5px; font-weight:700; letter-spacing:-.01em;
    white-space:nowrap; overflow:hidden; text-overflow:ellipsis; max-width:100%;
}
/* ⚠ (0,3,0), AND THIS IS THE BUG AARON SAW: "on some of the pages, every
   button is green". rip-ui.css repaints EVERY anchor on a .rip-ui page with
   `.rip-ui a:not(.rip-btn){color:var(--rip-act)}` at (0,2,1), which beats a bare
   `.rtb-tab` at (0,1,0) — so on those pages all four link tabs painted the
   action colour and only "Me" stayed grey, because Me is a <button> and the
   rule only matches anchors. That asymmetry is the fingerprint; the sheet items
   were already guarded and the tabs were not. Any new component that sets a
   link colour on a .rip-ui page needs the same treatment. */
.rtb a.rtb-tab:not(.rip-btn){ color:var(--rip-fg-3) }
.rtb-tab.is-on,
.rtb a.rtb-tab.is-on:not(.rip-btn){ color:var(--rip-act) }
/* the active marker is on the TOP edge, where the bar meets the page */
.rtb-tab.is-on::before{
    content:""; position:absolute; top:-6px; width:26px; height:3px;
    border-radius:0 0 3px 3px; background:var(--rip-act);
}
.rtb-tab:focus-visible{ outline:2px solid var(--rip-act); outline-offset:-3px; border-radius:9px }

/* Same pill as the burger's, same source (paintWaiting). */
.rtb-count{
    position:absolute; top:2px; left:calc(50% + 4px);
    min-width:16px; height:16px; padding:0 4px; border-radius:99px;
    background:var(--rip-bad,#fb2f5a); color:#fff;
    font-size:9.5px; font-weight:800; line-height:16px; text-align:center;
    font-variant-numeric:tabular-nums;
}

/* ── the Me sheet ─────────────────────────────────────────────────────────
   A sheet, not a page: it opens over what you were doing and closes on a tap,
   so the eight destinations that do not earn a tab cost one tap and no
   navigation. Everything in it is read from NAV, never re-listed, so a nav
   item added later cannot go missing from both surfaces. */
.rtb-scrim{ position:fixed; inset:0; z-index:9994; background:rgba(0,0,0,.55) }
/* ⚠ 9995 IS CHOSEN, NOT ARBITRARY: above #cookie-banner (9990), which is pinned
   to the same edge and would otherwise cover the sheet, and below #card-overlay
   (9997), because the reveal is a modal moment and nothing belongs on top of it.
   ⚠ AND IT SITS ABOVE THE BAR, NOT OVER IT — the tabs stay visible and the Me
   button stays where the thumb just left it, which is how it gets closed. */
.rtb-sheet{
    position:fixed; left:0; right:0; z-index:9995; box-sizing:border-box;
    bottom:calc(var(--rtb-h,64px) + env(safe-area-inset-bottom,0px));
    background:var(--rip-panel-2); border-top:1px solid var(--rip-line-2);
    border-radius:16px 16px 0 0;
    padding:10px 12px 12px;
    max-height:72vh; overflow-y:auto;
}
.rtb-sheet::before{
    content:""; display:block; width:34px; height:4px; border-radius:99px;
    background:var(--rip-line-2); margin:0 auto 12px;
}
.rtb-list{ display:grid; grid-template-columns:1fr 1fr; gap:8px; margin:0 }
@media(max-width:339px){ .rtb-list{ grid-template-columns:1fr } }
.rtb-item{
    display:flex; align-items:center; gap:9px; min-width:0;
    background:var(--rip-panel); border:1px solid var(--rip-line);
    border-radius:11px; padding:11px 10px; text-decoration:none;
    font-family:var(--rip-body); font-size:12.5px; font-weight:700;
    color:var(--rip-fg-2);
}
/* ⚠ (0,3,0) — rip-ui.css repaints every anchor at (0,2,1) and would otherwise
   make each of these cyan. Same treatment the header links already carry. */
.rtb-sheet a.rtb-item:not(.rip-btn){ color:var(--rip-fg-2) }
.rtb-item svg{ width:16px; height:16px; flex:0 0 auto; color:var(--rip-fg-3) }
.rtb-item span{ overflow:hidden; text-overflow:ellipsis; white-space:nowrap }
.rtb-item.is-on,.rtb-sheet a.rtb-item.is-on:not(.rip-btn){
    color:var(--rip-act); border-color:var(--rip-act)
}
.rtb-item.is-on svg{ color:var(--rip-act) }
/* The three the burger used to carry. Admin is gold because it is the one item
   only one person ever sees; Log out is the only destructive thing in here. */
.rtb-item--admin,.rtb-sheet a.rtb-item--admin:not(.rip-btn){
    color:var(--rip-val); border-color:rgba(242,177,52,.35)
}
.rtb-item--admin svg{ color:var(--rip-val) }
/* ⚠ AN EMPTY SLOT IS STILL A GRID CELL. For everyone who is not an admin the
   span holds nothing and was taking the left column, so Log out sat alone on
   the right with a hole beside it. */
/* ⚠ AN EMPTY SLOT IS STILL A GRID CELL. This is recorded for #rtb-admin-slot
   already — for a non-admin it took the left column and left Log out alone with
   a hole beside it — and #rtb-uni-slot shipped without the same rule, so every
   page that supplies no universes drew an empty cell in the top-left of the Me
   sheet. Aaron photographed exactly that on /rip/admin/ and on the pack page,
   2026-09-07. Both slots are filled asynchronously, so both need it: the hole is
   also what the sheet looks like for the instant before the fetch lands, and on
   any page whose fetch fails. */
/* ⚠ AND #rh-uni-slot IS THE SAME TRAP IN THE DESKTOP DROPDOWN. `.rh-menu-in`
   is a flex column with a gap, so an empty span there spends a row of nothing
   — the identical defect, in the identical shape, on the panel that gained a
   universe cross-link on 2026-09-08. Filled asynchronously like the other two,
   so it is empty on first paint and for ever on a page whose fetch fails. */
#rtb-admin-slot:empty,
#rtb-uni-slot:empty,
#rh-uni-slot:empty{ display:none }
.rtb-item--out{
    font-family:var(--rip-body); cursor:pointer;
    color:var(--rip-fg-3); text-align:left; width:100%
}
.rtb-who{
    margin:2px 2px 0; padding:0 0 4px;
    font-family:var(--rip-body); font-size:11.5px; color:var(--rip-fg-3);
    text-align:center; overflow:hidden; text-overflow:ellipsis; white-space:nowrap
}
.rtb-who b{ color:var(--rip-fg-2); font-weight:700 }

/* ⚠ CLEARANCE IS OWNED HERE, LIKE THE NAV'S. A page must not pin its own
   bottom padding: the bar's height is this file's business and nothing else
   can know it. --rtb-h is the painted height without the safe area. */
body.rtb-on{ --rtb-h:64px; padding-bottom:calc(var(--rtb-h) + env(safe-area-inset-bottom,0px)) }
@media(min-width:1060px){ body.rtb-on{ padding-bottom:0 } }

/* ⚠ TWO THINGS ON packs.htm ARE ALSO PINNED TO bottom:0 AND BOTH WOULD LAND ON
   TOP OF THIS BAR. (1,0,2) beats their own (1,0,0) whatever the source order.
   The cookie banner is the one that matters: it is z-index 9990, so it does not
   merely overlap the bar, it SWALLOWS EVERY TAP ON IT — and it is shown to
   first-time visitors, i.e. exactly the people this component exists for. Found
   by a headless click timing out with "cb-inner intercepts pointer events";
   nothing about it is visible in a screenshot, because the banner looks
   perfectly correct sitting where it always has. */
/* ⚠ THIS LIST IS THE WHOLE CONTRACT, AND IT MUST BE COMPLETE. A `position:fixed`
   element pinned to the bottom edge does NOT see body's padding-bottom, so every
   one of them lands under the bar. I shipped this list covering only the pack
   page's two, and `.tg-tray` — the tray that holds the SEND button on the trades
   pick screen — went straight under it. Aaron: "I cannot see the send button
   because of the new bar. you shouldn't make these kinds of mistakes ever."
   He is right: the sweep is one grep and I did not run it.

   ⚠ ADDING A BOTTOM-PINNED FIXED ELEMENT ANYWHERE UNDER /rip/ MEANS ADDING IT
   HERE. `rip/admin/cli/tabbar-check.js` greps for the pattern and fails on any
   selector that is not in this block, so a new one cannot ship unnoticed. */
body.rtb-on #sticky-open-bar,
/* The universe pages' own floating OPEN bar — rip/fun-pack.css's .fp-openbar,
   the same component on every universe page, so one entry covers all of them. */
body.rtb-on .fp-openbar,
body.rtb-on #cookie-banner,
body.rtb-on .tg-tray{ bottom:calc(var(--rtb-h) + env(safe-area-inset-bottom,0px)) }
/* Sits a tray's height above the tray, so it moves by exactly the same amount. */
body.rtb-on .tg-toast{
    bottom:calc(178px + var(--rtb-h) + env(safe-area-inset-bottom,0px));
}
@media(min-width:1060px){
    body.rtb-on #sticky-open-bar,
    body.rtb-on .fp-openbar,
    body.rtb-on #cookie-banner,
    body.rtb-on .tg-tray{ bottom:0 }
    body.rtb-on .tg-toast{ bottom:calc(178px + env(safe-area-inset-bottom,0px)) }
}

/* ══════════════════════════════════════════════════════════════════════════
   DURING A PULL, THE CHROME GETS OUT OF THE WAY (Aaron, 2026-08-31: "the pull
   is overshadowed by the buttons etc… completely fade out the button and also
   the new bottom bar when the card is being pulled? this would show off the
   card and also stop people clicking mid pull").

   ⚠ THE pointer-events ARE NOT DECORATION. The slab reveals in the page, under
   both of these, and both were tappable for the whole animation — the OPEN
   button most of all, which is why a mid-pull tap was so easy. Fading alone
   would leave an invisible button live over the card.

   ⚠ #sticky-open-bar IS ALREADY pointer-events:none WITH ITS BUTTON SET BACK TO
   auto, so the container is not the thing to disable — the child is.

   packs.htm owns the class (simSetAnimating), and the rule lives here because
   .rtb does: a page must not reach into the chrome's own styling.
   ══════════════════════════════════════════════════════════════════════════ */
/* ⚠ NOTHING ON THE PAGE IS CLICKABLE DURING A PULL, AND IT IS ONE RULE ON BODY
   RATHER THAN A LIST. Aaron, 2026-08-31, after the header survived the first
   pass: "I was just able to hit the pack dojo logo and return to the home page,
   which killed my pull… I don't want people clicking shit during the pull."

   The first version disabled the two elements I had thought of, which is the
   same shape of mistake as the tab-bar offset list: a named list is only ever
   as complete as whoever last remembered to add to it. `pointer-events:none` on
   BODY inherits to everything and cannot be forgotten by a component added
   later; the reveal overlay opts itself back in, and it is the only thing that
   should be live while a card is landing.

   Scrolling is unaffected — openPack() already sets overflow:hidden on both
   <html> and <body> for the duration. */
body.sim-pulling{ pointer-events:none }
body.sim-pulling #card-overlay{ pointer-events:auto }

body.sim-pulling .rtb,
body.sim-pulling #sticky-open-bar,
body.sim-pulling .fp-openbar,
/* ⚠ THE HEADER TOO. It is the last chrome left on screen, and its brand link
   navigates away — which is exactly how Aaron lost a pull. Faded rather than
   hidden so nothing reflows; the counters it carries are meaningless for the
   three seconds a card is landing anyway. */
body.sim-pulling .rh{
    opacity:0; pointer-events:none;
}
/* ⚠⚠ THE CHILDREN, NOT THE CONTAINER — AND .fp-openbar SHIPPED WITHOUT THIS FOR
   ONE TEST RUN. Both bars are pointer-events:none with their button set back to
   `auto`, and that `auto` RE-ENABLES the button against body.sim-pulling's
   blanket disable. So the faded bar stayed hit-testable over the landing card:
   an invisible live button, which is the precise defect Aaron reported when the
   brand link killed a pull. Measured with elementFromPoint at the button's own
   centre — true before this line, false after. */
body.sim-pulling #sticky-open-bar *,
body.sim-pulling .fp-openbar *{ pointer-events:none }
.rtb,#sticky-open-bar,.fp-openbar,.rh{ transition:opacity .45s ease }

@media(prefers-reduced-motion:reduce){
    .rtb-tab{ transition:none }
    /* Still hidden, just not animated — the point is the card, and the point is
       that nothing is tappable over it. */
    .rtb,#sticky-open-bar,.fp-openbar,.rh{ transition:none }
}
