/* ── METRIC-MATCHED FALLBACKS ────────────────────────────────────────────
   ⚠ THESE NUMBERS ARE MEASURED, NOT COPIED. Both pages' worst layout shift was
   one event at ~1700ms: the webfont arriving and resizing a headline. Measured
   at 390px, /rip/packs' hero title went 64px -> 35px as Bricolage swapped in
   (two lines collapsing to one) for a CLS of 0.34; the home page scored 0.49.
   Google's threshold is 0.10.

   Each face below pins its fallback with local(), so the size-adjust can be
   calibrated per platform: Roboto is what Android's sans-serif resolves to,
   Arial (Liberation Sans) is the desktop case. A face whose local() is absent
   is skipped and the next one wins, so both ship.

   Derived from canvas advance widths + fontBoundingBox at 1000px against the
   real webfonts, with Roboto pulled in as a webfont to be measured — this
   container has no Roboto, which is exactly why an earlier attempt at this was
   thrown away rather than shipped. Regenerate the same way if a weight is
   added; do not hand-edit a percentage.
   ──────────────────────────────────────────────────────────────────────── */
@font-face{font-family:'Outfit Fallback';src:local('Roboto');font-weight:300;font-style:normal;
  size-adjust:100.88%;ascent-override:99.12%;descent-override:25.77%;line-gap-override:0%}
@font-face{font-family:'Outfit Fallback';src:local('Arial');font-weight:300;font-style:normal;
  size-adjust:97.51%;ascent-override:102.55%;descent-override:26.66%;line-gap-override:0%}
@font-face{font-family:'Outfit Fallback';src:local('Roboto');font-weight:400;font-style:normal;
  size-adjust:100.36%;ascent-override:99.64%;descent-override:25.91%;line-gap-override:0%}
@font-face{font-family:'Outfit Fallback';src:local('Arial');font-weight:400;font-style:normal;
  size-adjust:97.97%;ascent-override:102.07%;descent-override:26.54%;line-gap-override:0%}
@font-face{font-family:'Outfit Fallback';src:local('Roboto');font-weight:700;font-style:normal;
  size-adjust:102.73%;ascent-override:97.34%;descent-override:25.31%;line-gap-override:0%}
@font-face{font-family:'Outfit Fallback';src:local('Arial');font-weight:700;font-style:normal;
  size-adjust:97.58%;ascent-override:102.48%;descent-override:26.64%;line-gap-override:0%}
@font-face{font-family:'Outfit Fallback';src:local('Roboto');font-weight:900;font-style:normal;
  size-adjust:105.32%;ascent-override:94.95%;descent-override:24.69%;line-gap-override:0%}
@font-face{font-family:'Outfit Fallback';src:local('Arial');font-weight:900;font-style:normal;
  size-adjust:100.74%;ascent-override:99.27%;descent-override:25.81%;line-gap-override:0%}
@font-face{font-family:'Bricolage Fallback';src:local('Roboto');font-weight:700;font-style:normal;
  size-adjust:95.33%;ascent-override:97.56%;descent-override:28.32%;line-gap-override:0%}
@font-face{font-family:'Bricolage Fallback';src:local('Arial');font-weight:700;font-style:normal;
  size-adjust:90.55%;ascent-override:102.71%;descent-override:29.82%;line-gap-override:0%}
@font-face{font-family:'Bricolage Fallback';src:local('Roboto');font-weight:800;font-style:normal;
  size-adjust:95.95%;ascent-override:96.93%;descent-override:28.14%;line-gap-override:0%}
@font-face{font-family:'Bricolage Fallback';src:local('Arial');font-weight:800;font-style:normal;
  size-adjust:91.78%;ascent-override:101.33%;descent-override:29.42%;line-gap-override:0%}
@font-face{font-family:'Figtree Fallback';src:local('Roboto');font-weight:400;font-style:normal;
  size-adjust:102.15%;ascent-override:93%;descent-override:24.47%;line-gap-override:0%}
@font-face{font-family:'Figtree Fallback';src:local('Arial');font-weight:400;font-style:normal;
  size-adjust:99.72%;ascent-override:95.27%;descent-override:25.07%;line-gap-override:0%}
@font-face{font-family:'Figtree Fallback';src:local('Roboto');font-weight:600;font-style:normal;
  size-adjust:102.44%;ascent-override:92.74%;descent-override:24.4%;line-gap-override:0%}
@font-face{font-family:'Figtree Fallback';src:local('Arial');font-weight:600;font-style:normal;
  size-adjust:97.3%;ascent-override:97.63%;descent-override:25.69%;line-gap-override:0%}
@font-face{font-family:'Figtree Fallback';src:local('Roboto');font-weight:700;font-style:normal;
  size-adjust:103.6%;ascent-override:91.69%;descent-override:24.13%;line-gap-override:0%}
@font-face{font-family:'Figtree Fallback';src:local('Arial');font-weight:700;font-style:normal;
  size-adjust:98.41%;ascent-override:96.53%;descent-override:25.4%;line-gap-override:0%}
@font-face{font-family:'Figtree Fallback';src:local('Roboto');font-weight:800;font-style:normal;
  size-adjust:104.26%;ascent-override:91.11%;descent-override:23.98%;line-gap-override:0%}
@font-face{font-family:'Figtree Fallback';src:local('Arial');font-weight:800;font-style:normal;
  size-adjust:99.73%;ascent-override:95.25%;descent-override:25.07%;line-gap-override:0%}

/* ══════════════════════════════════════════════════════════════════════════
   rip-ui.css — the Pack Simulator's own design system.

   DELIBERATELY NOT the Roaring Standard (rs-style.css). The simulator is its
   own product: rs-style.css is streamer-facing — loud red, gold, pixel type,
   photo heroes. This is collector-facing, and it behaves like a collection
   app: a calm ink ground, dense data, and the CARDS supplying all the colour.

   Do not import rs-style.css alongside this. Do not reintroduce the red
   headline / gold prize pairing here — red is reserved for destructive
   actions only, so it still means "careful" and never "click me".

   Tokens first, components after. Anything new composes from the tokens.
   ══════════════════════════════════════════════════════════════════════════ */

:root{
  /* ground — ink with a blue bias, so the neutrals read as chosen not default */
  --rip-bg:#0a0e15;
  --rip-bg-2:#070a10;
  --rip-panel:#111721;
  --rip-panel-2:#18202c;
  --rip-panel-3:#212b3a;
  --rip-line:rgba(255,255,255,.075);
  --rip-line-2:rgba(255,255,255,.15);

  /* ink */
  --rip-fg:#eef3f9;
  --rip-fg-2:#97a4b8;
  --rip-fg-3:#66738a;

  /* ACTION — holo cyan. The one thing you click. */
  --rip-act:#2ee6d6;
  --rip-act-2:#14b8a6;
  --rip-act-ink:#04201d;
  --rip-act-soft:rgba(46,230,214,.13);
  --rip-act-line:rgba(46,230,214,.42);

  /* VALUE — amber. Money and market prices only, never a button. */
  --rip-val:#f2b134;
  --rip-val-soft:rgba(242,177,52,.13);

  /* semantic, kept away from the accent so state still reads at a glance */
  --rip-up:#4ade80;
  --rip-down:#fb7185;
  --rip-danger:#e11d48;          /* destructive only */

  /* tier colours are DATA, not decoration — same values the binder uses */
  --rip-t-starter:#7c3aed;
  --rip-t-silver:#94a3b8;
  --rip-t-gold:#f59e0b;
  --rip-t-sensei:#0f52ba;
  --rip-t-platinum:#c0ccd8;
  --rip-t-diamond:#38bdf8;
  --rip-t-lunar:#818cf8;
  --rip-t-neutronium:#e11d48;
  --rip-t-retired:#8b93a7;

  --rip-r:14px;                  /* panel radius */
  --rip-r-sm:9px;
  --rip-shadow:0 18px 44px rgba(0,0,0,.55);
  --rip-maxw:1120px;
  /* The one vertical step between top-level blocks on every /rip/ page. See
     the .rip-wrap stack rule for why it is a gap and not a pile of margins.
     36 by default, 28 under 560px — this sheet is written desktop-first and
     narrows in the max-width:560px block, the same direction .rip-wrap's own
     padding already steps (26/72 -> 22/56). */
  --rip-stack:36px;

  --rip-display:'Bricolage Grotesque','Bricolage Fallback','Figtree','Figtree Fallback',ui-sans-serif,system-ui,sans-serif;
  --rip-body:'Figtree','Figtree Fallback',ui-sans-serif,system-ui,-apple-system,sans-serif;
  /* ⚠ --rip-data IS NO LONGER A MONO FACE. Aaron, 2026-08-11: "remove JetBrains
     font from everywhere on the packdojo site, I don't like it." The token is
     kept — ~100 rules consume it and it still means "this is data" — but it now
     resolves to the same stack as --rip-display. Anything that needs digits to
     line up in a column should say `font-variant-numeric:tabular-nums`, which
     is what actually delivers that and does not require a monospaced face. */
  --rip-data:'Bricolage Grotesque','Bricolage Fallback','Figtree','Figtree Fallback',ui-sans-serif,system-ui,sans-serif;
}

/* ── base ─────────────────────────────────────────────────────────────── */
.rip-ui{
  background:
    radial-gradient(1200px 620px at 50% -8%, rgba(46,230,214,.07), transparent 62%),
    var(--rip-bg);
  color:var(--rip-fg);
  font-family:var(--rip-body);
  font-size:16px; line-height:1.55;
  -webkit-font-smoothing:antialiased;
  min-height:100vh; margin:0;
}
/* ⚠ SPACING IS SET HERE FOR EVERY /rip/ PAGE. Aaron reviews on a phone and
   called the welcome page "very tight" (2026-08-08); the same numbers were
   feeding every other page through this sheet, so they were loosened here
   rather than page by page. Measured at 412px before: a 13px side gutter and
   14px between a section heading and the content it introduces. */
/* ⚠ box-sizing IS SET HERE, NOT INHERITED. Without it max-width caps the
   CONTENT box, so the wrap renders 1120 + 36px of padding = 1156 — which is
   exactly what /rip/leaderboard measured against /rip/collectors' 1120,
   because collectors carries a global *{box-sizing:border-box} reset and
   leaderboard does not. The same trap already cost the tier bar 8px (see
   rip/CLAUDE.md). Pinning it on the component makes the page's own reset
   irrelevant. */
.rip-wrap{box-sizing:border-box;max-width:var(--rip-maxw);margin:0 auto;padding:26px 18px 72px}

/* ── ONE GAP DOWN THE PAGE, AND IT IS THE ONLY AUTHORITY ─────────────────
   Aaron, 2026-08-19, having asked three times: "vertically there's not enough
   space between one thing and the next. it's not acceptable."

   Measured at 390px before this rule, top-level gaps inside .rip-wrap:
     leaderboard  [0, 0, 0, 40, 14]      collectors  [22, 18, 18, 34, 40, 14]
     stats        [0, 33]
   Nothing was wrong with any single number. The fault was that there was no
   number — every block set its own margin-top, so the rhythm changed as you
   scrolled and the page read as a pile rather than a stack. That is what he
   was pointing at on the leaderboard screenshot: three sections touching,
   then a 68px chasm.

   ⚠ ONE STEP, SET IN ONE PLACE. Every block used to pick its own margin-top,
   so a new section was a fresh chance to pick a different number, and adjacent
   margins collapse — the value in the sheet was often not the value on screen.
   Zeroing them all and re-adding a single step between siblings means a new
   section inherits the rhythm by existing.

   ⚠ NOT A FLEX COLUMN, AND THIS WAS MEASURED THE WRONG WAY FIRST. `display:flex
   + gap` is the tidier expression of "one step" and it produced a perfect
   uniform 28px — while silently destroying the page widths. `margin:0 auto` on
   a flex item resolves the auto margins against the free space instead of
   stretching, so leaderboard's `#lb-players{max-width:600px;margin:0 auto}`
   shrink-wrapped to its content: 316px inside a 1084px column, 29%, against
   600px before. Two other sections went the same way. The vertical audit was
   green throughout, because it was not looking at width.
   `> * + *` keeps ordinary block layout — auto-margin centring, max-width caps
   and percentage widths all behave exactly as they did — and changes nothing
   but the step.

   ⚠ DOUBLED CLASS, DELIBERATELY. `.rip-wrap > *` is (0,1,0) and ties with the
   page sheets' own `.cd-tools{margin-top:22px}` / `.rip-crawl{margin-top:40px}`,
   which load AFTER this file and would therefore win. `.rip-wrap.rip-wrap > *`
   is (0,2,0) and beats a single class whatever the source order — so the step
   cannot be silently re-fought page by page. An inline `style="margin-top:…"`
   still beats both, which is why those were removed from the blocks that had
   them (rip/lib/chrome.php, collectors.php) rather than fought with here. */
.rip-wrap.rip-wrap > *{margin-top:0;margin-bottom:0}
.rip-wrap.rip-wrap > * + *{margin-top:var(--rip-stack)}

/* A lede straight after a section heading is that heading's subtitle, not the
   next block down the page — the full step between them reads as two unrelated
   things. browse.php, set.php and card.php were each cancelling it by hand with
   an inline `margin:-8px 0 0`, which is the same fix written three times where
   nobody can find it and where it beats any rule that tries to correct it.
   (0,3,0), so it wins over the step above without needing !important. */
.rip-wrap.rip-wrap > .rip-sec + .rip-lede{margin-top:8px}

/* ── THE PAGE HEAD. ONE OF THEM. ─────────────────────────────────────────
   Aaron, 2026-08-10, first time on a PC: "binder/ has a blue subheader, and
   then a nice header, but the page width is capped. leaderboard/ has a grey
   subheader... but the page is full width. collectors/ ... there's a larger gap
   before the subheader and header start. Make your mind up, make everything
   uniform, and keep it that way going forward."

   He was looking at THREE systems:
     · member pages  — main.wrap (860px) + .mp-head__eyebrow/__h1/__sub, a
                       parallel type scale (10.5px / 15.5px) to the shared one
     · collectors    — .cd-head{padding:26px 0 4px}, its own top gap
     · leaderboard   — sections carrying max-width:1120px INSIDE an already
                       capped, padded main, so <main> measured 1156px on a
                       1280px screen while its siblings measured 1120

   There is one now. A page opens with .rip-page-head wrapping .rip-eyebrow,
   .rip-h1 and .rip-lede — no page-specific padding, no second type scale, and
   no child that re-caps a width its parent has already capped. */
.rip-page-head{padding:0 0 4px;margin:0}
.rip-page-head .rip-eyebrow{display:block;margin:0}
.rip-page-head .rip-h1{margin:8px 0 0}
.rip-page-head .rip-lede{margin:12px 0 26px;max-width:56ch}

/* THE HEAD'S SUPPORTING PARAGRAPHS. The component only ever described a head of
   eyebrow + h1 + one lede, so the hub pages (rip/hubs.php — /rip/cards/psa-10,
   /rip/packs/gold and 200-odd more) — which open with a lede AND one or two
   paragraphs qualifying it — wrote `style="margin:10px 0 0;color:…;
   font-size:14.5px;max-width:70ch"` on each instead. That is an inline margin
   on a direct child of .rip-wrap, the one thing the page step cannot outrank,
   and the measured result was a page opening 28/12/10/10/22/20 where every
   other page opens 28: three TIGHT findings from tools/spacing-audit.js.
   Named here so the head owns its own rhythm, like every other member of it.
   14px, not the 36/28 page step: these belong to the lede, not to the page.
   ⚠ A CLASS, NOT `.rip-page-head p`. A bare-element selector there is (0,1,1)
   and would beat `.rip-eyebrow` (0,1,0) on the four pages whose eyebrow is a
   <p>, repainting it at 14.5px in the body colour. */
.rip-page-head__p{margin:14px 0 0;color:var(--rip-fg-2);font-size:14.5px;max-width:62ch}
/* ⚠ THE 26px TAIL BELONGS TO WHATEVER ENDS THE HEAD, NOT TO THE LEDE — MOVE
   IT, DO NOT DUPLICATE IT AND DO NOT DROP IT. Left on the lede it opens a 26px
   hole in the middle of one block, against the 14px between the paragraphs
   below it. Dropped altogether, a hub page's head would sit 26px closer to the
   page than every other /rip/ page's head does, which trades one inconsistency
   for another. :has() carries its argument's specificity, so the first of these
   is (0,3,0) and beats `.rip-page-head .rip-lede` (0,2,0) without !important. */
.rip-page-head .rip-lede:has(+ .rip-page-head__p){margin-bottom:0}
.rip-page-head__p:last-child{margin-bottom:26px}

/* ⚠ A SECTION INSIDE .rip-wrap MUST NOT SET ITS OWN max-width. The wrap is
   already --rip-maxw with 18px of padding, so a 1120px child bursts out of it
   by exactly that padding. This is the rule leaderboard.php broke. */
.rip-wrap > section{max-width:none}
.rip-ui h1,.rip-ui h2,.rip-ui h3{
  font-family:var(--rip-display); font-weight:800; letter-spacing:-.025em;
  line-height:1.06; margin:0; text-wrap:balance;
}
.rip-h1{font-size:clamp(30px,6.4vw,52px)}
.rip-h2{font-size:clamp(20px,3.4vw,28px)}
.rip-h3{font-size:17px;letter-spacing:-.012em}
.rip-lede{color:var(--rip-fg-2);font-size:16px;max-width:64ch;margin:0}
.rip-num{font-family:var(--rip-data);font-variant-numeric:tabular-nums;font-weight:800}
.rip-eyebrow{
  font-family:var(--rip-data); font-size:11px; font-weight:700;
  letter-spacing:.18em; text-transform:uppercase; color:var(--rip-fg-3);
}
/* :not(.rip-btn) matters — a bare `.rip-ui a` rule outranks `.rip-btn--primary`
   on specificity and repaints the button label in the accent colour, which on
   the accent-filled button is all but invisible. */
.rip-ui a:not(.rip-btn){color:var(--rip-act);text-decoration:none}
.rip-ui a:not(.rip-btn):hover{text-decoration:underline}
.rip-ui :focus-visible{outline:2px solid var(--rip-act);outline-offset:2px;border-radius:4px}

/* ── panel: the slab. A notched corner instead of a plain rounded box. ── */
.rip-panel{
  background:linear-gradient(180deg,var(--rip-panel-2),var(--rip-panel));
  border:1px solid var(--rip-line);
  border-radius:var(--rip-r);
  padding:18px;
  position:relative;
}
.rip-panel--notch{
  border-radius:var(--rip-r) var(--rip-r) var(--rip-r) 0;
  clip-path:polygon(0 0,100% 0,100% 100%,18px 100%,0 calc(100% - 18px));
}
.rip-panel--flat{background:var(--rip-panel);}

/* ── buttons ──────────────────────────────────────────────────────────── */
.rip-btn{
  display:inline-flex;align-items:center;justify-content:center;gap:9px;
  font-family:var(--rip-body);font-size:15px;font-weight:800;letter-spacing:.005em;
  padding:13px 22px;border-radius:999px;border:1px solid transparent;
  cursor:pointer;transition:transform .12s,box-shadow .15s,background .15s,color .15s;
  text-decoration:none;white-space:nowrap;
}
.rip-btn:hover{text-decoration:none;transform:translateY(-1px)}
.rip-btn:active{transform:translateY(0)}
.rip-btn--primary{
  background:linear-gradient(135deg,var(--rip-act),var(--rip-act-2));
  color:var(--rip-act-ink);
  box-shadow:0 6px 22px rgba(46,230,214,.26);
}
.rip-btn--primary:hover{box-shadow:0 10px 30px rgba(46,230,214,.36)}
.rip-btn--ghost{background:var(--rip-panel-2);border-color:var(--rip-line-2);color:var(--rip-fg)}
.rip-btn--ghost:hover{border-color:var(--rip-act-line);color:var(--rip-act)}
.rip-btn--danger{background:transparent;border-color:var(--rip-danger);color:var(--rip-danger)}
.rip-btn--lg{font-size:16.5px;padding:16px 30px}

/* ── chips ────────────────────────────────────────────────────────────── */
.rip-chip{
  display:inline-flex;align-items:center;gap:6px;
  font-family:var(--rip-data);font-size:10.5px;font-weight:700;
  letter-spacing:.09em;text-transform:uppercase;
  padding:4px 10px;border-radius:999px;
  border:1px solid var(--rip-line-2);color:var(--rip-fg-3);white-space:nowrap;
}
.rip-chip--act{color:var(--rip-act);border-color:var(--rip-act-line);background:var(--rip-act-soft)}
.rip-chip--val{color:var(--rip-val);border-color:var(--rip-val);background:var(--rip-val-soft)}
.rip-chip--tier{color:#0b0d12;border:0;font-size:10px;padding:4px 9px}

/* ── stat tile: number first, label second. Scanned, not read. ── */
.rip-stats{display:grid;grid-template-columns:repeat(auto-fit,minmax(140px,1fr));gap:10px}
.rip-stat{
  background:var(--rip-panel);border:1px solid var(--rip-line);
  border-radius:var(--rip-r-sm);padding:14px 16px;
}
.rip-stat__v{font-family:var(--rip-data);font-size:26px;font-weight:800;line-height:1.1;font-variant-numeric:tabular-nums}
.rip-stat__v--val{color:var(--rip-val)}
.rip-stat__v--act{color:var(--rip-act)}
.rip-stat__l{
  font-size:11px;font-weight:700;letter-spacing:.11em;text-transform:uppercase;
  color:var(--rip-fg-3);margin-top:5px;
}

/* ── progress ─────────────────────────────────────────────────────────── */
.rip-prog{height:10px;border-radius:5px;background:var(--rip-panel-3);overflow:hidden}
.rip-prog__fill{
  height:100%;border-radius:5px;
  background:linear-gradient(90deg,var(--rip-act-2),var(--rip-act));
  box-shadow:0 0 16px rgba(46,230,214,.45);
}

/* ── the sleeve — the product's signature object ──────────────────────── */
.rip-sleeve{
  position:relative;aspect-ratio:5/7;border-radius:10px;overflow:hidden;
  background:linear-gradient(155deg,rgba(255,255,255,.055),rgba(255,255,255,.012) 45%,rgba(0,0,0,.3));
  box-shadow:inset 0 0 0 1px rgba(255,255,255,.10),inset 0 2px 7px rgba(0,0,0,.5);
}
.rip-sleeve::after{                       /* plastic sheen */
  content:'';position:absolute;inset:0;pointer-events:none;z-index:4;border-radius:10px;
  background:linear-gradient(118deg,rgba(255,255,255,.20) 0%,rgba(255,255,255,.05) 16%,
             transparent 34%,transparent 72%,rgba(255,255,255,.07) 100%);
  mix-blend-mode:screen;opacity:.72;
}
.rip-sleeve__card{position:absolute;inset:4px;border-radius:7px;overflow:hidden;
  border:1.5px solid var(--tc,var(--rip-line-2));background:var(--rip-panel-2)}
.rip-sleeve__card img{width:100%;height:100%;object-fit:cover;display:block}

/* ── CARD CORNERS, EVERYWHERE ─────────────────────────────────────────────
   Every card scan in the store is an OPAQUE RECTANGLE: the card's rounded
   corner is already cut and the space outside it filled WHITE. On a dark page
   that puts four bright right-angles on every card in the app, the binder, the
   leaderboard, the collector pages and the welcome page.

   One attribute selector fixes all of them at once, because every one of those
   surfaces emits the same URL shape — <img src="/card-image.php?h=…">. No
   markup change, no per-page edit, and no reprocessing of the thousands of
   stored WebPs: the pixels stay exactly as they are and the browser simply
   stops painting the corners.

   ⚠ TWO radii, not one. A single percentage on a non-square box gives an
   ELLIPTICAL corner. A card is 63x88mm, so the vertical radius has to be the
   horizontal one times 63/88 (0.716) for the arc to come out circular at any
   rendered size: 4.8% x 0.716 = 3.44%.

   4.8% is deliberately wider than the ~4.1% the scans themselves round at
   (measured off a 734px source), so the cut clears the white on every card
   rather than leaving a sliver on the ones that round tightest.

   ⚠ This reaches the DOM only. Canvas compositing (binder.php's share sheet)
   and the server-rendered share images do their own rounding — see clipR()
   there and rip_og_card() in rip/og-lib.php. */
.rip-ui img[src*="card-image.php"],
img[src*="card-image.php"]{border-radius:4.8% / 3.44%}
.rip-sleeve__foot{
  position:absolute;bottom:0;left:0;right:0;z-index:3;pointer-events:none;
  background:linear-gradient(transparent,rgba(0,0,0,.93));padding:22px 8px 7px;
}
.rip-sleeve__name{font-size:11px;font-weight:800;color:#fff;line-height:1.25;
  overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.rip-sleeve__row{display:flex;align-items:center;gap:5px;margin-top:3px}
.rip-sleeve__val{font-family:var(--rip-data);font-size:11px;font-weight:800;
  color:var(--rip-val);margin-left:auto}
/* the empty pocket — dashed means missing, everywhere in this product */
.rip-sleeve--empty{background:linear-gradient(155deg,rgba(255,255,255,.03),rgba(0,0,0,.35))}
.rip-sleeve--empty .rip-sleeve__slot{
  position:absolute;inset:4px;border-radius:7px;border:1.5px dashed rgba(255,255,255,.13);
  display:flex;align-items:center;justify-content:center;
  font-size:30px;font-weight:800;color:rgba(255,255,255,.13);
}

/* ── badge — an achievement, locked or earned ─────────────────────────── */
.rip-badges{display:grid;grid-template-columns:repeat(auto-fill,minmax(148px,1fr));gap:10px}
.rip-badge{
  background:var(--rip-panel);border:1px solid var(--rip-line);
  border-radius:var(--rip-r-sm);padding:14px;text-align:center;position:relative;
}
.rip-badge__icon{
  width:46px;height:46px;margin:0 auto 9px;border-radius:12px;
  display:flex;align-items:center;justify-content:center;font-size:23px;
  background:var(--rip-panel-3);
}
.rip-badge__name{font-size:13.5px;font-weight:800;line-height:1.3}
.rip-badge__desc{font-size:11.5px;color:var(--rip-fg-3);margin-top:3px;line-height:1.4}
.rip-badge__rar{font-family:var(--rip-data);font-size:10px;font-weight:700;letter-spacing:.07em;
  color:var(--rip-fg-3);margin-top:9px;padding-top:8px;border-top:1px solid var(--rip-line)}
.rip-badge--earned .rip-badge__rar{color:var(--rip-act)}
.rip-badge--earned{border-color:var(--rip-act-line);background:linear-gradient(180deg,var(--rip-act-soft),var(--rip-panel) 62%)}
.rip-badge--earned .rip-badge__icon{background:var(--rip-act-soft);box-shadow:0 0 20px rgba(46,230,214,.24)}
/* Legends wear GOLD, the value colour — the one place a badge is allowed to
   borrow it, because a legend IS the premium thing on the page. Never a
   button: gold stays non-interactive everywhere in this system. */
.rip-badge--legend.rip-badge--earned{border-color:rgba(242,177,52,.5);
  background:linear-gradient(180deg,rgba(242,177,52,.13),var(--rip-panel) 62%)}
.rip-badge--legend.rip-badge--earned .rip-badge__icon{background:rgba(242,177,52,.16);
  box-shadow:0 0 22px rgba(242,177,52,.3)}
.rip-badge--legend.rip-badge--earned .rip-badge__rar{color:var(--rip-val)}
.rip-badges__more{margin:12px 0 0;text-align:center;font-family:var(--rip-data);font-size:12px;
  font-weight:700;letter-spacing:.06em;color:var(--rip-fg-3)}
.rip-badge--locked{opacity:.44}
.rip-badge--locked .rip-badge__icon{filter:grayscale(1)}

/* ── card grid used by public pages ───────────────────────────────────── */
.rip-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(112px,1fr));gap:10px}

/* ── data table ───────────────────────────────────────────────────────────
   The aggregate hub pages (/cards/psa-10, /packs/<tier>, the value bands) are
   RANKED LISTS OF NUMBERS, so they are real <table>s — a crawler reads a table
   as a table, and a screen reader gets row/column relationships a grid of divs
   cannot express.

   ⚠ THE WRAPPER IS NOT OPTIONAL. A five-column table of card names cannot fit
   390px at a readable size, and a table that overflows its container scrolls the
   BODY horizontally — measured: document.scrollWidth 604 vs innerWidth 390 on the
   first draft, which drags the whole page sideways including the header. The
   wrapper takes the scroll instead, so the page itself never moves.

   Money is amber (--rip-val) and nothing else in the table is, so "how much"
   stays scannable down the column without reading a single label. */
.rip-tablewrap{overflow-x:auto;-webkit-overflow-scrolling:touch;
  border:1px solid var(--rip-line);border-radius:var(--rip-r-sm);background:var(--rip-panel)}
.rip-table{width:100%;border-collapse:collapse;font-size:13.5px}
.rip-table th,.rip-table td{padding:10px 13px;text-align:left;vertical-align:top;
  border-bottom:1px solid var(--rip-line);white-space:nowrap}
.rip-table thead th{
  font-family:var(--rip-data);font-size:10.5px;font-weight:800;letter-spacing:.11em;
  text-transform:uppercase;color:var(--rip-fg-3);background:var(--rip-panel-2);
  position:sticky;top:0;z-index:1;
}
.rip-table tbody tr:last-child td{border-bottom:0}
.rip-table tbody tr:hover{background:rgba(255,255,255,.022)}
.rip-table__n{font-family:var(--rip-data);font-variant-numeric:tabular-nums;
  font-weight:800;color:var(--rip-fg-3);text-align:right;width:1%}
.rip-table__v{font-family:var(--rip-data);font-variant-numeric:tabular-nums;
  font-weight:800;color:var(--rip-val);text-align:right}
/* The second line of a cell — a set under a card name, a year under a set. It
   carries the column the table does NOT spend a whole column on, which is how
   five columns stay inside a phone. */
.rip-table__sub{display:block;font-size:11.5px;color:var(--rip-fg-3);
  margin-top:2px;font-weight:400}
/* (0,3,1) beats `.rip-ui a:not(.rip-btn)` at (0,2,1) — see the note at that
   rule. Without it every card name in a 100-row table is cyan and the table
   reads as 100 calls to action. */
.rip-ui .rip-table a:not(.rip-btn){color:var(--rip-fg);font-weight:700}
.rip-ui .rip-table a:not(.rip-btn):hover{color:var(--rip-act)}
.rip-ui .rip-table__sub a:not(.rip-btn){color:var(--rip-fg-3);font-weight:400}

/* ── hub link strip — the sibling hubs, as chips ──────────────────────────
   Same (0,3,1) problem and the same fix: a chip sets its own colour at (0,1,0)
   and loses to the global anchor rule, so a row of eight pack chips came out
   uniformly cyan and the CURRENT one was indistinguishable from the rest. */
.rip-hublinks{display:flex;flex-wrap:wrap;gap:7px;margin:14px 0 0;padding:0;list-style:none}
.rip-ui .rip-hublinks a.rip-chip:not(.rip-btn){color:var(--rip-fg-2)}
.rip-ui .rip-hublinks a.rip-chip:not(.rip-btn):hover{color:var(--rip-act);border-color:var(--rip-act-line)}
.rip-ui .rip-hublinks [aria-current="page"]{color:var(--rip-act);border-color:var(--rip-act-line);
  background:var(--rip-act-soft)}

/* ── section head ─────────────────────────────────────────────────────── */
/* The bottom margin is the one that mattered: 14px put a section's heading
   almost against its own first row, so the heading read as belonging to the
   block ABOVE it. The top margin separates sections; the bottom one has to be
   clearly smaller than it but still legible as a gap. 40/18 holds that ratio. */
.rip-sec{display:flex;align-items:baseline;justify-content:space-between;gap:12px;
  flex-wrap:wrap;margin:40px 0 18px}
.rip-sec__t{display:flex;align-items:baseline;gap:11px;flex-wrap:wrap}

/* ── footer ───────────────────────────────────────────────────────────── */
.rip-foot{margin-top:46px;padding-top:20px;border-top:1px solid var(--rip-line);
  color:var(--rip-fg-3);font-size:12.5px;line-height:1.65}

/* ── crawl nav (rip_chrome_nav()) ─────────────────────────────────────────
   ⚠ THIS IS THE SERVER-RENDERED SITE NAV AND ITS WHOLE POINT IS THAT IT IS IN
   THE HTML. Measured 2026-08-10: https://packdojo.com/ returned 3 <a> tags in
   67,862 bytes and /rip/cards had ZERO inbound links anywhere in raw markup —
   the header and footer are both injected by JS (rip-header.js /
   rip-footer.js), so a crawler that does not run scripts reached the entire
   card catalogue by no path at all. This row is the non-JS floor: real <a
   href> to every public destination, emitted by PHP.
   It is NOT a second footer. rip-footer.js bails when it finds an existing
   `.rf`, so this must never carry that class. */
/* Was an inline style on the <p> that rip/lib/chrome.php emits, which no
   stylesheet can outrank — see the note there. A single class instead, so the
   .rip-wrap stack gap wins when this is a top-level block and the 14px still
   applies when it is nested inside a section. */
.rip-legal{margin:14px 0 0}

.rip-crawl{margin-top:40px;padding-top:18px;border-top:1px solid var(--rip-line);
  display:flex;flex-wrap:wrap;gap:6px 18px;font-size:13px}
/* (0,3,1), which BEATS `.rip-ui a:not(.rip-btn)` at (0,2,1) — see the note at
   that rule. A bare `.rip-crawl a` is (0,1,1) and would lose, painting all six
   links cyan so a plain nav row reads as six calls to action. */
.rip-ui .rip-crawl a:not(.rip-btn){color:var(--rip-fg-2)}
.rip-ui .rip-crawl a:not(.rip-btn):hover{color:var(--rip-act)}
.rip-ui .rip-crawl a[aria-current="page"]{color:var(--rip-fg);font-weight:700}

@media (max-width:560px){
  /* 18px of side gutter, not 13. A phone has less room, which is the argument
     for a smaller TYPE scale, not for running the content into the bezel. */
  .rip-wrap{padding:22px 18px 56px}
  :root{--rip-stack:28px}
  .rip-sec{margin:32px 0 16px}
  .rip-panel{padding:16px}
  .rip-stat__v{font-size:22px}
  .rip-foot{margin-top:38px;padding-top:18px}
}
@media (prefers-reduced-motion:reduce){
  .rip-btn,.rip-btn:hover{transition:none;transform:none}
}

/* ── TAP-SIZE FLOOR ───────────────────────────────────────────────────────
   40px is the smallest control a thumb hits reliably, and these pages are
   used on a phone mid-stream. A control whose own sheet loads AFTER this one
   cannot be raised from its own file when that file is not ours to edit, so
   the floor lives here — this is the one sheet every /rip/ page loads, and it
   loads first. The selector is doubled to (0,2,0) so it beats the (0,1,0)
   declaration in the later sheet on specificity rather than on source order,
   which is the half that would silently flip if load order ever changed.

   .badge-chip (the difficulty filter row on /rip/badges) declares
   min-height:36px in rip/account.css. Only the BOX grows: the type, the
   horizontal padding and the white-space:nowrap width are untouched, so the
   strip still scrolls sideways at exactly the same width. */
.badge-chip.badge-chip{min-height:40px}

/* ═══════════════════════════════════════════════════════════════════════════
   PAGE HEAD + UNIVERSE SWITCH — shared, because they were the drift.

   ⚠ AARON, 2026-09-07: "the marvel page still looks completely different to
   the pokemon one. You're pissing me off because I keep asking for unity and
   for you to not miss things." He is right, and the CAUSE was structural, not
   carelessness about one page: `.pk-head` and `.uni-switch` were defined in
   packs.htm's own inline <style>, so NO OTHER PAGE COULD USE THEM. A component
   that lives inside one page is a component every other page has to re-invent,
   and re-inventing it is exactly how two pages on one site stop matching.

   Both live here now, in the sheet every /rip/ page already loads, and both
   pages render the same classes. They cannot diverge again without somebody
   editing this block.

   ⚠ NAMED `.rip-*` ON PURPOSE. rip-ui.css's contract is that every selector is
   either :root or a .rip-* class — that is what makes adding this sheet to a
   page safe (it can define tokens and repaint nothing). `.pk-head` would have
   broken the naming half of that promise even though it is still a class.
   ═══════════════════════════════════════════════════════════════════════════ */
.rip-pagehead{padding:16px 0 18px}
.rip-pagehead__eyebrow{font-family:var(--rip-data);font-size:10.5px;font-weight:700;
    letter-spacing:.18em;text-transform:uppercase;color:var(--rip-act);margin:0 0 10px}
.rip-pagehead__h1{font-family:var(--rip-display);font-weight:800;letter-spacing:-.03em;
    line-height:1.04;font-size:clamp(30px,7.6vw,44px);color:var(--rip-fg);margin:0;
    text-wrap:balance}
.rip-pagehead__sub{color:var(--rip-fg-2);font-size:15.5px;line-height:1.55;margin:11px 0 0;
    max-width:52ch;text-wrap:pretty}
@media(min-width:760px){ .rip-pagehead{padding:22px 0 22px} }

/* ⚠ NO PIXEL FONT IN THE EYEBROW. The Marvel page used Press Start 2P here and
   the pack page used Bricolage, which is most of why they read as two sites.
   The pixel face is a .rs-tag accent at >=13px, never a component's own type. */

/* The universe switch: Pokémon | Marvel | … above whatever the page's main
   control is. Two full-width tap targets, because the whole point is that a
   member who has only ever seen Pokémon NOTICES there is now something else. */
.uni-switch{display:grid;grid-template-columns:1fr 1fr;gap:8px;margin:0 0 12px}
/* A third universe wraps to its own row rather than squeezing three into
   390px — the labels are words, not numbers. */
@media (min-width:560px){ .uni-switch{grid-template-columns:repeat(auto-fit,minmax(160px,1fr))} }
/* ⚠ (0,2,0) AND :not(.rip-btn). This sheet repaints every anchor on a .rip-ui
   page at (0,2,1); a bare .uni-btn loses that tie and the whole switch comes
   out cyan — the exact failure already recorded for the header and the tab bar. */
.uni-switch a.uni-btn:not(.rip-btn){
    display:flex;flex-direction:column;gap:1px;justify-content:center;
    /* ⚠ 76, NOT 52. Aaron, 2026-09-07: "the height is too small … make the
       image more visible in terms of transparency and also the height of the
       box." The art is scaled to the button's height, so the box height IS the
       size of the picture — 52 gave a 25px band of card and nothing read. */
    min-height:76px;padding:11px 13px;border-radius:12px;text-decoration:none;
    background:var(--rip-bg-2,#11151f);border:1px solid var(--rip-line,#232a3a);
    color:var(--rip-fg,#e8eaf2);text-align:left;
    /* ⚠ isolation, NOT overflow alone. The art below is a z-index:-1 child so
       it sits above this element's own background and below its text without
       either of them needing a z-index; a negative child only stays inside the
       button because this establishes a stacking context. */
    position:relative;overflow:hidden;isolation:isolate}
/* ⚠⚠ THE BACKGROUND IS REAL CARDS FROM THAT UNIVERSE (Aaron, 2026-09-07:
   "Please add a pokemon background and marvel background to those boxes,
   subtly"). One composited .webp per universe, ~9KB, served from our own
   origin — the canonical image rule forbids depending on a third-party CDN,
   and these are cropped from cards already in the local store.

   ⚠⚠ THE FADE IS A MASK IN BUTTON SPACE, AND IT USED TO BE BAKED INTO THE FILE.
   The baked ramp could not be lost by a CSS edit, which was the point — but it
   lives in IMAGE space, and `background-size:auto 100%` scales the art by the
   button's height. So the taller box Aaron asked for squashed the ramp into a
   smaller fraction of the button and started pushing the picture in under the
   words. A mask on this layer is measured against the button at whatever size
   it happens to be, at 175px wide and at 620px, short or tall. Keep it: without
   it the label sits on a picture.

   ⚠ `--uni-art` IS SET PER BUTTON BY WHOEVER RENDERS THE SWITCH, so a universe
   with no art file gets no ::after content and the button is exactly what it
   was before this shipped. There is no per-universe rule in this sheet, and a
   third universe needs none. */
.uni-switch a.uni-btn:not(.rip-btn)::after{
    content:'';position:absolute;inset:0;z-index:-1;pointer-events:none;
    background-image:var(--uni-art,none);
    /* ⚠ `auto 100%`, NOT `cover`, AND THAT IS A MEASUREMENT. These buttons are
       175px wide at 390px and ~620px at 1280 — the grid is auto-fit inside a
       980px column. `cover` scales to the WIDER axis, so at 1280 it blew the
       art up 1.7x and cropped it to a horizontal smear. Scaling to the height
       keeps the cards at their own size at every width and simply leaves more
       dark space on the left of a wide button, which is where the words are. */
    background-size:auto 100%;background-position:right center;background-repeat:no-repeat;
    /* The words occupy roughly the left half at 390px, so the art is gone by
       32% and only fully itself past 80%. Both -webkit- and the standard
       property: Safari still needs the prefix for mask-image. */
    -webkit-mask-image:linear-gradient(to right,transparent 0,transparent 32%,#000 80%);
            mask-image:linear-gradient(to right,transparent 0,transparent 32%,#000 80%);
    /* ⚠ .46, RAISED FROM .20 ON HIS WORD — "you can barely see the image".
       The first pass was tuned against a 2x element screenshot, which is not
       what a phone shows; measured on the page it was a faint wash. It is still
       bounded: the mask means this only ever applies to the right of the
       label, so a higher number costs legibility nothing. */
    opacity:.46}
.uni-switch a.uni-btn.is-on:not(.rip-btn)::after{opacity:.54}
@media (prefers-contrast:more){
    /* A picture behind text is the first thing to go when someone has asked
       for more contrast. */
    .uni-switch a.uni-btn:not(.rip-btn)::after{opacity:.14}
}
.uni-switch a.uni-btn:not(.rip-btn) b{font-size:14.5px;font-weight:800;line-height:1.2}
/* The sub-line is the first thing to go if it cannot fit; the name alone still
   says which universe the button opens. */
.uni-switch a.uni-btn:not(.rip-btn) span{
    font-size:11px;color:var(--rip-dim,#98a1b3);line-height:1.3;
    overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.uni-switch a.uni-btn:not(.rip-btn):hover{border-color:var(--uni-acc,var(--rip-act,#2dd4bf))}
.uni-switch a.uni-btn.is-on:not(.rip-btn){
    border-color:var(--rip-act,#2dd4bf);
    box-shadow:inset 0 0 0 1px var(--rip-act,#2dd4bf)}

/* ── gloss + glint — the sealed-pack surface ──────────────────────────────
   ⚠ THIS IS WHY THE MARVEL PACK LOOKED "VERY BLAND" (Aaron, 2026-09-07: "make
   the pack size on the marvel page the same as the pokemon page. same shimmer,
   effects etc"). Measured on both packs at rest: the Pokemon pack runs TWO
   animations (`sbGlint`, `sbGlint2`) over a six-gradient gloss layer; the
   Marvel pack ran ZERO and had no gloss at all. A flat gradient with a border
   is a rectangle; this is what makes it read as a laminated foil pack.

   ⚠ IT LIVES HERE, NOT IN A PAGE'S <style>, BECAUSE THE LAST ROUND OF THIS
   COMPLAINT WAS THE SAME BUG. The page head and the universe switch were
   locked inside packs.htm's inline <style> "where nothing else could reach
   it", which is how the two pages drifted; both were moved here for that
   reason. A treatment two surfaces share belongs in the shared sheet.

   ⚠ packs.htm STILL CARRIES ITS OWN COPY as `.sb-gem` / `.sb-glint*`, and that
   is a known, recorded duplicate — not an oversight. Rewiring the live pull
   page's pack back is a bigger risk than this ask carries (that page has nine
   reverted "fixes" on the record), so it adopts `.rs-gloss` the next time that
   block is genuinely being edited. Until then, a change here needs the same
   change there.

   ⚠ NO 3D ANCESTOR. packs.htm's version is a SIBLING of the flipping shell on
   purpose: inside a `transform-style:preserve-3d` / `backface-visibility:hidden`
   element a transform stops being re-rastered while its style keeps updating,
   so the band looks frozen — that was the actual fault in all four earlier
   attempts there. A host that flips its pack must mount this outside the
   flipping element; `.fp-pack` does not flip, so it mounts inside. */
.rs-gloss{position:absolute;inset:0;pointer-events:none;overflow:hidden;
    border-radius:inherit;
    background:
      linear-gradient(107deg, transparent 30%, rgba(255,255,255,0.05) 42%,
                      rgba(255,255,255,0.11) 47%, rgba(255,255,255,0.04) 53%, transparent 64%),
      radial-gradient(78% 46% at 22% 10%, rgba(255,255,255,0.15) 0%, rgba(255,255,255,0.05) 40%, transparent 72%),
      radial-gradient(26% 15% at 30% 13%, rgba(255,255,255,0.13) 0%, transparent 70%),
      linear-gradient(180deg, rgba(255,255,255,0.18) 0px, rgba(255,255,255,0) 2px),
      linear-gradient(90deg, rgba(255,255,255,0.09) 0px, rgba(255,255,255,0) 2px),
      radial-gradient(120% 90% at 88% 96%, rgba(0,0,0,0.30) 0%, transparent 60%)}
/* ⚠ THE CLIP LAYER IS MASKED, AND THAT IS WHAT MAKES IT LOOK REAL. overflow
   alone ends the band with a dead straight cut along the edge — the one thing
   a real specular never does. The radial mask fades anything approaching any
   edge to nothing, so the band appears out of the surface rather than sliding
   in from outside the frame. It sits on this wrapper and not on .rs-gloss
   because the gloss WANTS to reach the edges: the rim highlights along the top
   and left are edge effects by definition. */
.rs-glint-clip{position:absolute;inset:0;overflow:hidden;border-radius:inherit;
    -webkit-mask:radial-gradient(112% 96% at 50% 50%,#000 30%,rgba(0,0,0,0.6) 66%,transparent 96%);
    mask:radial-gradient(112% 96% at 50% 50%,#000 30%,rgba(0,0,0,0.6) 66%,transparent 96%)}
/* ⚠ A UNIFORM TRANSLUCENT BAND IS WHY EVERY EARLIER ATTEMPT LOOKED FAKE.
   1. Huge dynamic range along the width — a narrow bright core inside a very
      wide, very faint falloff, not an even ramp (0 → .015 → .05 → .22 → …).
   2. Not even along its LENGTH: a flat card is never perfectly flat, so the
      highlight dies toward the ends. That is what stops it reading as a ruler
      laid across the card.
   3. Not pure white — glancing reflections run cooler than the surface under
      them, and the faint blue lift is the difference between "lit" and
      "painted on".
   And a SECOND, fainter, offset band, because a laminated card has two
   reflecting surfaces and the real thing always shows a weak echo trailing the
   main highlight. */
.rs-glint,.rs-glint2{position:absolute;top:-45%;bottom:-45%;left:0;
    -webkit-mask:linear-gradient(180deg,transparent 0%,#000 26%,#000 74%,transparent 100%);
    mask:linear-gradient(180deg,transparent 0%,#000 26%,#000 74%,transparent 100%)}
.rs-glint{width:38%;
    background:linear-gradient(100deg,transparent 0%,rgba(255,255,255,0.015) 20%,
               rgba(255,255,255,0.05) 38%,rgba(226,240,255,0.22) 50%,
               rgba(255,255,255,0.05) 62%,rgba(255,255,255,0.015) 80%,transparent 100%);
    transform:rotate(18deg) translateX(-140%);
    animation:rsGlint 5.6s cubic-bezier(.42,0,.58,1) infinite}
.rs-glint2{width:16%;opacity:0.55;
    background:linear-gradient(100deg,transparent 0%,rgba(255,255,255,0.03) 40%,
               rgba(214,232,255,0.10) 50%,rgba(255,255,255,0.03) 60%,transparent 100%);
    transform:rotate(18deg) translateX(-260%);
    animation:rsGlint2 5.6s cubic-bezier(.42,0,.58,1) infinite}
/* ⚠ THE TRAVEL IS SIZED SO THE BAND IS FULLY OFF THE CARD AT BOTH ENDS. The
   tier bar hit the same trap: stopping on the arithmetic boundary parks a
   visible sliver at the edge for the whole rest. */
@keyframes rsGlint {0%{transform:rotate(18deg) translateX(-140%)}
                    64%,100%{transform:rotate(18deg) translateX(430%)}}
@keyframes rsGlint2{0%{transform:rotate(18deg) translateX(-260%)}
                    64%,100%{transform:rotate(18deg) translateX(760%)}}
@media(prefers-reduced-motion:reduce){.rs-glint,.rs-glint2{animation:none}}
