/* ===================================================================
   mobile.css — the phone shell for the character creator
   ===================================================================
   Ported from the phone mockups in /mockups. One screen, three bands:

     hero    a crimson banner carrying the progress rail, the question
             and (where there is one) a live quota
     body    whichever of the four views the step resolves to — a card
             rail, an option list, the level rail, the ability flow —
             or, for step types with no bespoke phone design yet, the
             desktop markup hosted inside the same shell
     pad     the action, pinned at the bottom where a thumb already is

   Colours are theme tokens, not the mockups' literals: --crimson,
   --bark, --parchment and friends are aliases every theme redefines
   (shared/theme.css), so the parchment palette the mockups were drawn
   in is simply one theme's rendering of these rules.

   The shell is position:fixed under the top bar rather than inside the
   scrolling page: every view here measures itself against the height it
   actually got, and a band that can be scrolled away can't be measured.
   `zoom` on :root means viewport units render at 80%, so anything
   spanning the viewport divides by --sb-ui-scale (see navbar.css).
   =================================================================== */

/* The shell owns the screen: nothing behind it may scroll. !important
   because styles.css's small-screen rules force body.wizard-active to
   overflow:visible for the desktop markup — that's what let a swipe on the
   card rails drag the whole page (and the browser bar) up and down whenever
   a finger drifted off horizontal. This file loads after, so it wins. */
/* Screen only: paper has no viewport to pin the page to, and a printed
   document held at one screen tall with its overflow hidden loses everything
   past the first page. */
@media screen {
  /* position:fixed, not just overflow:hidden — iOS Safari happily pans a
     page whose layout runs taller than the visible viewport whatever its
     overflow says, and that slack let the whole shell wander up and down
     a thumb's width. A fixed body has nowhere to go. */
  body.sbm-on { overflow: hidden !important; height: 100% !important;
    position: fixed !important; top: 0 !important; left: 0 !important;
    right: 0 !important; bottom: 0 !important; width: 100% !important; }
  html:has(body.sbm-on) { overflow: hidden !important; height: 100% !important;
    overscroll-behavior: none; }
}
/* The wizard's own desktop chrome is replaced wholesale, not restyled. */
body.sbm-on .app-header,
body.sbm-on .breadcrumb-row,
body.sbm-on > footer { display: none !important; }
body.sbm-on .wizard-container { padding: 0 !important; margin: 0 !important; }

.sbm {
  position: fixed;
  top: var(--sb-topbar-h, 0px);
  left: 0; right: 0; bottom: 0;
  /* bottom:0 pins to the LAYOUT viewport, which on iOS runs on under
     Safari's expanded bottom bar — the action button was ending up half
     behind it. An explicit height in dynamic-viewport units ends the
     shell at the VISIBLE bottom instead, so the button is always fully
     on screen (height beats bottom when both are set). Divided by
     --sb-vh-fit — the MEASURED factor the root zoom applies to viewport
     units on this engine (Chrome scales them, iOS Safari doesn't; see
     _sbmMeasureVhFit in mobile.js). */
  height: calc(100vh / var(--sb-vh-fit, var(--sb-ui-scale, 1)) - var(--sb-topbar-h, 0px));
  /* Percentages carry no unit ambiguity: whatever an engine makes of the
     height above, the shell can never run taller than the screen. */
  max-height: calc(100% - var(--sb-topbar-h, 0px));
  z-index: 40;
  display: flex;
  flex-direction: column;
  background: var(--bark-dark);
  color: var(--ink);
  font-family: 'Crimson Text', Georgia, serif;
  overflow: hidden;
  --sbm-on-accent: var(--text-on-accent, #F5E6D0);
}
@supports (height: 100dvh) {
  .sbm { height: calc(100dvh / var(--sb-vh-fit, var(--sb-ui-scale, 1)) - var(--sb-topbar-h, 0px)); }
}
/* Embedded in play.html there is no top bar to sit under. */
html.sb-embed .sbm { top: 0; height: calc(100vh / var(--sb-vh-fit, var(--sb-ui-scale, 1))); max-height: 100%; }
@supports (height: 100dvh) {
  html.sb-embed .sbm { height: calc(100dvh / var(--sb-vh-fit, var(--sb-ui-scale, 1))); }
}
.sbm * { box-sizing: border-box; }

/* ── hero ──────────────────────────────────────────────────────────
   The card views put one word here (the breadcrumb's own label for the
   step); a choice step has to say what is being chosen and how many, so
   it carries the full question, a rule, the subtitle and a live count.
   --tscale is the type's share of the height budget: when the browser's
   URL bar appears the banner and the rows come down a notch together
   rather than anything being clipped. */
.sbm-hero {
  position: relative;
  flex-shrink: 0;
  background: var(--crimson);
  text-align: center;
  padding: 22px 22px 18px;
  overflow: hidden;
}
.sbm-hero.sbm-hero-word {
  display: flex; align-items: center; justify-content: center;
  height: var(--hero-h, 126px);
  padding: 0 64px;
}
.sbm.hero-min .sbm-hero-word .sbm-ttl { display: none; }
/* Room on BOTH sides for whichever icons are present, so the centred title
   stays centred and a title that wraps to two lines still clears them. */
.sbm-hero:has(.sbm-ico) { padding-left: 64px; padding-right: 64px; }
/* A step whose subtitle runs long would otherwise push the list off the
   screen: the banner keeps a ceiling and the explanation scrolls inside it,
   so nothing is truncated and the choices stay reachable. */
.sbm-hero-text { max-height: 38vh; overflow-y: auto; scrollbar-width: none;
  overscroll-behavior: contain; }
.sbm-hero-text::-webkit-scrollbar { display: none; }

.sbm-prail { position: absolute; top: 0; left: 0; right: 0; height: 6px;
  background: rgba(var(--shade-rgb), .18); }
.sbm-prail i { display: block; height: 6px; background: var(--sbm-on-accent);
  width: var(--prog, 8%); transition: width .45s cubic-bezier(.4, 0, .2, 1); }

.sbm-ttl {
  margin: 0; font-weight: 400; line-height: 1.15;
  color: var(--sbm-on-accent); text-transform: uppercase;
  font-size: calc(27px * var(--tscale, 1)); letter-spacing: 3px;
}
.sbm-hero-word .sbm-ttl { font-size: calc(40px * var(--tscale, 1)); letter-spacing: 5px; }
/* A rule between the question and its explanation. The title is uppercase
   and letterspaced, the subtitle sentence case — without a break they read
   as one run-on paragraph. */
.sbm-rule { width: 76px; height: 1px; background: rgba(var(--shade-rgb), .28);
  margin: calc(13px * var(--tscale, 1)) auto calc(12px * var(--tscale, 1)); }
.sbm-sub { margin: 0; color: var(--sbm-on-accent); opacity: .82;
  font-size: calc(15px * var(--tscale, 1)); line-height: 1.45; }
.sbm-rules { margin: 10px 0 0; padding-left: 18px; text-align: left;
  color: var(--sbm-on-accent); opacity: .8; font-size: calc(13.5px * var(--tscale, 1)); line-height: 1.5; }

.sbm-count {
  display: inline-block; margin-top: 11px; padding: 4px 13px;
  font-family: system-ui, sans-serif; font-size: 12px; letter-spacing: 1.5px;
  text-transform: uppercase; color: var(--sbm-on-accent);
  border: 1px solid rgba(var(--shade-rgb), .3); border-radius: 999px;
}
.sbm-count.done { background: var(--sbm-on-accent); color: var(--crimson);
  border-color: var(--sbm-on-accent); }

/* Hero icon buttons: back on the left, search on the right. The hero
   reserves the same room on both sides so the centred title stays
   centred whether or not either is present. */
.sbm-ico {
  position: absolute; width: 44px; height: 44px; padding: 0;
  border-radius: 10px; border: 1.5px solid rgba(var(--shade-rgb), .3);
  background: transparent; color: var(--sbm-on-accent);
  display: flex; align-items: center; justify-content: center;
  cursor: pointer; -webkit-tap-highlight-color: transparent;
}
.sbm-ico svg { width: 21px; height: 21px; stroke: currentColor; fill: none;
  stroke-width: 2; stroke-linecap: round; stroke-linejoin: round; }
.sbm-back { left: 12px; top: 14px; }
.sbm-find { right: 12px; bottom: 12px; }
/* The dice mirror the back chevron: top-right on the tall hero, vertically
   centred on the one-word hero. The find button keeps the bottom-right. */
.sbm-dice { right: 12px; top: 14px; }
.sbm-hero-word .sbm-back { top: 50%; margin-top: -22px; }
.sbm-hero-word .sbm-dice { top: 50%; margin-top: -22px; }
.sbm.searching .sbm-find { background: var(--sbm-on-accent); color: var(--crimson);
  border-color: var(--sbm-on-accent); }

/* ── the action ────────────────────────────────────────────────── */
/* The shell ends at the VISIBLE bottom now (see _sbmFitShellHeight), so
   the pad no longer needs standing clearance for Safari's bar — just a
   comfortable rest, or the home-indicator inset when the page truly runs
   to the screen edge (standalone mode). */
.sbm-pad { flex-shrink: 0; padding: 8px 24px max(14px, env(safe-area-inset-bottom, 0px)); }
.sbm-ink {
  width: 100%; height: 62px; border-radius: 10px;
  border: 1.5px solid var(--crimson); background: transparent; color: var(--crimson);
  font-family: 'Crimson Text', Georgia, serif; font-size: 19px;
  letter-spacing: 2.5px; text-transform: uppercase; cursor: pointer;
  /* Ink fills from the bottom while the button is held. --fill is driven a
     frame at a time from script, so the fill tracks the finger and drains
     back if it leaves early — a transition here would coast past the release
     and let a quick tap through. */
  overflow: hidden;
  box-shadow: inset 0 calc(-1 * var(--sbm-h, 62px) * var(--sbm-fill, 0)) 0 0 var(--crimson);
  transition: box-shadow .06s linear, color .12s ease, opacity .16s ease;
  -webkit-tap-highlight-color: transparent;
  /* Holding is the gesture — a long press must never turn into the browser
     selecting the label or offering a context menu instead. */
  user-select: none; -webkit-user-select: none; -webkit-touch-callout: none;
}
/* The label flips once the ink is past it, not on press — so how far it has
   filled is readable at a glance. */
.sbm-ink.sbm-past-half { color: var(--sbm-on-accent); }
.sbm-ink.sbm-fired { animation: sbmInkPulse .62s cubic-bezier(.18, 1.4, .3, 1); }
@keyframes sbmInkPulse { 0% { transform: scale(1); } 24% { transform: scale(1.13); } 100% { transform: scale(1); } }
/* Absolute inside the shell at the button's own layout numbers, not fixed
   at viewport coordinates: the root zoom and any viewport pan both slide
   the viewport grid out from under getBoundingClientRect, and the ring
   was landing high of the button it celebrates. The shell spans the whole
   screen, so its overflow clips the ring exactly where the screen edge
   would have. */
.sbm-inkburst { position: absolute; z-index: 60; border-radius: 12px;
  border: 3px solid var(--crimson); pointer-events: none;
  box-shadow: 0 0 34px 10px rgba(var(--accent-rgb), .5), inset 0 0 22px 4px rgba(245, 230, 208, .35); }
/* Tap buttons arrive already filled — the outline-that-fills is the hold
   gesture's own look, so a button wearing the full colour reads as "just
   press me" at a glance. */
.sbm-ink.sbm-tap { background: var(--crimson); color: var(--sbm-on-accent); }
.sbm-ink.sbm-tap:active { filter: brightness(1.14); }
/* The nudge for someone tapping a hold button like a tap button: a bubble
   off the button itself, where their thumb already is — not a toast at the
   far edge of the screen. */
.sbm-holdhint { position: absolute; z-index: 62; transform: translate(-50%, calc(-100% - 12px));
  max-width: 270px; padding: 11px 15px; border-radius: 12px;
  background: var(--parchment); border: 1.5px solid var(--crimson); color: var(--ink);
  font-family: 'Crimson Text', Georgia, serif; font-size: 15.5px; line-height: 1.4;
  text-align: center; box-shadow: 0 8px 22px rgba(var(--shade-rgb), .35);
  cursor: pointer; user-select: none; -webkit-user-select: none;
  animation: sbmHintPop .34s cubic-bezier(.2, 1.4, .4, 1); }
.sbm-holdhint b { color: var(--crimson); font-weight: 600; }
.sbm-holdhint::after { content: ''; position: absolute; left: 50%; bottom: -7px;
  width: 12px; height: 12px; margin-left: -6px; background: var(--parchment);
  border-right: 1.5px solid var(--crimson); border-bottom: 1.5px solid var(--crimson);
  transform: rotate(45deg); }
@keyframes sbmHintPop { 0% { opacity: 0; transform: translate(-50%, calc(-100% - 2px)) scale(.85); }
  100% { opacity: 1; transform: translate(-50%, calc(-100% - 12px)) scale(1); } }
/* Not disabled — dimmed. A disabled button on a phone gives no feedback at
   all when pressed; this one still responds and says what's missing. */
.sbm-ink.waiting { opacity: .45; }
.sbm-ink[hidden] { display: none; }
/* The count lives under the button, not in it: a changing label makes the
   button change width and weight every time you pick something, and that is
   the one control that should never move. Hidden rather than removed, so the
   button can't hop up the screen when the line empties. */
.sbm-note { margin: 5px 0 0; min-height: 15px; text-align: center;
  font-size: 12.5px; line-height: 1.2; font-style: italic; color: var(--gold-dim); }
.sbm-note.spent { visibility: hidden; }

/* ===================================================================
   View: the card rail — race, class, subrace, subclass, background
   =================================================================== */
.sbm-railwrap { flex: 1; min-height: 0; position: relative; }
/* overscroll-behavior-x:contain suppresses iOS's own rubber band so ours is
   the only one, and stops an overswipe turning into the browser's back
   gesture. */
.sbm-strip {
  display: flex; gap: 26px; align-items: center; height: 100%;
  overflow-x: auto; scroll-snap-type: x mandatory; overscroll-behavior-x: contain;
  /* An overflow-x of auto silently promotes overflow-y from visible to AUTO,
     which quietly makes the rail a vertical scroller too — the bleed layer
     already stands 27px proud of each card, so the moment the rail is even
     slightly shorter than the artwork a drifting swipe scrolls it. Pinned
     to hidden so that scrollport can never exist; overscroll-behavior-y
     stops a vertical drag here handing off to the page or the address bar.
     (overflow-y:clip would keep the scrollport away without clipping, but a
     box that scrolls on the other axis computes clip back to hidden, so
     hidden is what this is. The rail is laid out with ~34px of slack around
     the halo, so hidden costs nothing visually.) */
  overflow-y: hidden; overscroll-behavior-y: none;
  -webkit-overflow-scrolling: touch; scrollbar-width: none;
  /* On rails: a swipe whose finger drifts up or down is still a swipe.
     pan-x hands the browser ONLY horizontal panning here, so the drift
     can't leak into moving anything underneath the cards. Repeated on the
     cards because the touch lands on THEM, and not every engine walks the
     ancestor chain the same way. */
  touch-action: pan-x;
}
.sbm-strip .sbm-card, .sbm-strip .sbm-lc { touch-action: pan-x; }
/* The one vertical scroll a card owns: reading a flipped card's back. */
.sbm-card.flipped .sbm-cardback { touch-action: pan-y; }
.sbm-strip::-webkit-scrollbar { display: none; }

/* THE BLEED — two copies of the picture sized from the same variables so
   they stay in register: .sbm-face is the crisp copy clipped to the card,
   .sbm-bleed the oversized copy spilling out behind it. --card-window is
   the card's width as a fraction of the PICTURE's width; the art is square
   and sized `auto 100%`, so it renders exactly --card-h wide. Everything
   comes off that one number, so the card can shrink for a shorter viewport
   without the two layers drifting apart. The bleed sits OUTSIDE the flipper:
   the halo belongs to the rail, not to the face showing. */
.sbm-card {
  --card-h: 340px; --card-window: .75; --card-scale: 1.16;
  --bleed-opacity: 1; --neighbour-opacity: .38;
  flex: 0 0 calc(var(--card-h) * var(--card-window)); height: var(--card-h);
  /* snap-stop:always makes every card a hard stop, so a hard flick advances
     one card instead of coasting past four. */
  scroll-snap-align: center; scroll-snap-stop: always;
  position: relative; background: none; border: none; padding: 0;
  cursor: pointer; perspective: 1100px;
  -webkit-tap-highlight-color: transparent;
}
.sbm-bleed {
  position: absolute; z-index: 0; pointer-events: none;
  top: calc((1 - var(--card-scale)) / 2 * 100%);
  bottom: calc((1 - var(--card-scale)) / 2 * 100%);
  left: calc((var(--card-window) - var(--card-scale)) / 2 * var(--card-h));
  right: calc((var(--card-window) - var(--card-scale)) / 2 * var(--card-h));
  background-image: var(--img); background-position: center;
  background-repeat: no-repeat; background-size: auto 100%;
  opacity: 0; transition: opacity .38s ease;
  -webkit-mask-image: linear-gradient(to bottom, transparent 0%, black 8%, black 92%, transparent 100%), linear-gradient(to right, transparent 0%, black 27%, black 73%, transparent 100%);
  -webkit-mask-composite: destination-in;
  mask-image: linear-gradient(to bottom, transparent 0%, black 8%, black 92%, transparent 100%), linear-gradient(to right, transparent 0%, black 27%, black 73%, transparent 100%);
  mask-composite: intersect;
}
/* The centre rule must come last and match the neighbour rule's weight, or
   the neighbour rule would win on the selected card too. */
.sbm-strip.bleed-neighbours .sbm-card .sbm-bleed { opacity: var(--neighbour-opacity); }
.sbm-strip .sbm-card.is-centre .sbm-bleed { opacity: var(--bleed-opacity); }

.sbm-flip { position: absolute; inset: 0; z-index: 1; transform-style: preserve-3d;
  transition: transform .55s cubic-bezier(.4, .05, .2, 1); }
.sbm-card.flipped .sbm-flip { transform: rotateY(180deg); }
.sbm-face, .sbm-cardback {
  position: absolute; inset: 0; backface-visibility: hidden;
  -webkit-backface-visibility: hidden;
  border: 2px solid var(--crimson); border-radius: 14px; overflow: hidden;
}
.sbm-face { background-image: var(--img); background-position: center;
  background-repeat: no-repeat; background-size: auto calc(100% * var(--card-scale));
  background-color: var(--bark); }
.sbm-cardback { transform: rotateY(180deg); background: var(--parchment);
  display: flex; flex-direction: column; text-align: left; }
/* A card with no art of its own is named on its face, since the caption
   underneath is the only other thing that would name it. */
.sbm-card.noart .sbm-face { display: flex; align-items: center; justify-content: center;
  padding: 16px; background-image: none; }
.sbm-face-plate { font-family: 'Cinzel', Georgia, serif; font-size: 21px;
  letter-spacing: 1px; color: var(--crimson); text-align: center; line-height: 1.25; }

/* The "+ Create custom …" tile — a stop on the rail like any other. */
.sbm-card.add .sbm-face {
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  gap: 14px; background-image: none; background-color: transparent;
  border: 2px dashed var(--crimson); color: var(--crimson);
}
.sbm-card.add .sbm-plus { font-size: 74px; line-height: 1; }
.sbm-card.add .sbm-lb { font-size: 19px; letter-spacing: 2px; text-transform: uppercase; text-align: center; }
.sbm-card-upload { margin-top: 4px; font-size: 12px; padding: 6px 12px; }

/* Back of the card: the campaign library's detail format, at card scale. */
.sbm-info-head { padding: 12px 13px 0; flex-shrink: 0; }
.sbm-info-name { font-family: 'Cinzel', Georgia, serif; font-size: 19px; font-weight: 700;
  color: var(--ink); line-height: 1.15; }
.sbm-info-kind { font-size: 11.5px; color: var(--gold-dim); font-style: italic; margin-top: 1px; }
.sbm-info-tags { display: flex; flex-wrap: wrap; gap: 4px; margin-top: 6px; }
.sbm-info-tag {
  --tag-c: hsl(var(--tag-h, 0) 75% 40%);
  font-family: 'Cinzel', Georgia, serif; font-size: 8px; font-weight: 700;
  letter-spacing: 1px; text-transform: uppercase; padding: 2px 6px;
  border-radius: 3px; white-space: nowrap;
  color: color-mix(in srgb, var(--tag-c) 50%, var(--ink));
  background: color-mix(in srgb, var(--tag-c) 16%, transparent);
  border: 1px solid color-mix(in srgb, var(--tag-c) 42%, transparent);
}
.sbm-info-scroll {
  overflow-y: auto; flex: 1; min-height: 0; padding: 10px 13px 14px;
  -webkit-mask-image: linear-gradient(to bottom, black 0%, black 88%, transparent 100%);
  mask-image: linear-gradient(to bottom, black 0%, black 88%, transparent 100%);
}
.sbm-info-scroll::-webkit-scrollbar { display: none; }
.sbm-info-subs { margin-bottom: 12px; }
.sbm-info-subs-label { font-family: 'Cinzel', Georgia, serif; font-size: 9.5px; font-weight: 700;
  color: var(--gold); letter-spacing: .5px; text-transform: uppercase; margin-bottom: 5px; }
.sbm-info-subs-list { display: flex; flex-wrap: wrap; gap: 5px; }
.sbm-info-chip { background: rgba(var(--accent-rgb), .12); border: 1px solid var(--border);
  border-radius: 999px; padding: 2px 9px; font-size: 11.5px; color: var(--ink); }

.sbm-facts { display: grid; grid-template-columns: 1fr 1fr; gap: 6px; margin: 0 0 4px; }
.sbm-fact { background: rgba(var(--accent-rgb), .05); border: 1px solid var(--border);
  border-radius: 7px; padding: 7px 9px; }
.sbm-fact.wide { grid-column: 1 / -1; }
.sbm-fact-k { font-family: 'Cinzel', Georgia, serif; font-size: 8px; font-weight: 700;
  letter-spacing: 1.1px; text-transform: uppercase; color: var(--gold-dim); margin-bottom: 3px; }
.sbm-fact-v { font-size: 12.5px; font-weight: 600; line-height: 1.3; color: var(--ink); }
.sbm-pills { display: flex; flex-wrap: wrap; align-items: baseline; gap: 3px 16px; }
.sbm-pill { display: inline-flex; align-items: baseline; gap: 5px; line-height: 1.3; }
.sbm-pill-k { font-family: 'Cinzel', Georgia, serif; font-size: 8.5px; font-weight: 700;
  letter-spacing: .7px; text-transform: uppercase; color: var(--gold-dim); }
.sbm-pill-v { font-size: 13px; font-weight: 700; color: var(--ink); }

.sbm-sec { margin-top: 15px; }
.sbm-sec-h { display: flex; align-items: center; gap: 8px; font-family: 'Cinzel', Georgia, serif;
  font-size: 10.5px; font-weight: 900; letter-spacing: 2px; text-transform: uppercase;
  color: var(--crimson); margin-bottom: 9px; }
.sbm-sec-h::before { content: ""; width: 3px; height: 12px; background: var(--crimson);
  border-radius: 2px; flex: none; }
.sbm-sec-h::after { content: ""; flex: 1; height: 2px;
  background: linear-gradient(to right, var(--border), transparent); }
.sbm-item { position: relative; padding: 0 0 0 15px; margin-bottom: 11px; }
.sbm-item:last-child { margin-bottom: 0; }
.sbm-item::before { content: ""; position: absolute; left: 1px; top: 6px; width: 6px; height: 6px;
  background: var(--gold); transform: rotate(45deg); border-radius: 1px; }
.sbm-item-name { font-family: 'Cinzel', Georgia, serif; font-size: 12.5px; font-weight: 700;
  letter-spacing: .4px; color: var(--gold); margin-bottom: 2px; }
.sbm-item-text { font-size: 11.5px; line-height: 1.5; color: var(--ink-mid); }
.sbm-item-text p { margin: 0 0 6px; }
.sbm-item-text p:last-child { margin-bottom: 0; }

/* ── position dots, sitting on the name's shoulder ─────────────── */
.sbm-dots { flex: 0 0 24px; display: flex; align-items: center; justify-content: center; gap: 6px; }
.sbm-dot { width: 7px; height: 7px; border-radius: 50%; background: var(--border); flex: none;
  border: none; padding: 0; cursor: pointer;
  transition: width .25s ease, background-color .25s ease, border-radius .25s ease; }
.sbm-dot.on { width: 20px; border-radius: 4px; background: var(--crimson); }

/* ── name + blurb ──────────────────────────────────────────────────
   One block, not two: as two rows each centring its own content there was
   50px of manufactured air between them. Together in one centred column
   they sit a deliberate 9px apart. This block is what gives way when the
   browser bar appears — height and type come down together, leaving the
   artwork alone. */
.sbm-caption { flex: 0 0 var(--text-h, 150px); display: flex; flex-direction: column;
  align-items: center; justify-content: center; padding: 0 20px; overflow: hidden; }
.sbm-caption b { font-size: calc(30px * var(--tscale, 1)); letter-spacing: 3px;
  color: var(--crimson); text-transform: uppercase; font-weight: 400; line-height: 1.1;
  max-width: 100%; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.sbm-caption p { font-size: calc(18px * var(--tscale, 1)); line-height: 1.4;
  color: var(--ink-mid); text-align: center; margin: calc(9px * var(--tscale, 1)) 0 0; }

/* ── the hints ─────────────────────────────────────────────────────
   A solid white hand under a hairline of near-black: the card behind it is
   a photograph, so nothing tinted survives every option. Every card carries
   its own, so whichever you have swiped to says the same thing. It's a CSS
   loop rather than a driven one — nothing to start, stop or cancel, and it
   can't fight the flip's own transform. */
.sbm-fing, .sbm-tapring, .sbm-scrollhint { position: absolute; pointer-events: none; z-index: 3; }
.sbm-fing svg, .sbm-scrollhint svg { fill: #fff; stroke: #15100c; stroke-width: .9;
  stroke-linejoin: round; filter: drop-shadow(0 2px 5px rgba(0, 0, 0, .4)); }
.sbm-fing { left: 50%; top: 50%; margin: -26px 0 0 -30px; width: 60px; height: 68px;
  animation: sbmTapLoop 2.4s ease-in-out infinite; }
.sbm-fing svg { width: 60px; height: 68px; }
.sbm-tapring { left: 50%; top: 50%; margin: -30px 0 0 -30px; width: 60px; height: 60px;
  border-radius: 50%; border: 2.5px solid #fff;
  box-shadow: 0 0 0 1px rgba(21, 16, 12, .6), inset 0 0 0 1px rgba(21, 16, 12, .6);
  opacity: 0; animation: sbmTapRing 2.4s ease-out infinite; }
@keyframes sbmTapLoop { 0% { transform: translateY(0); } 6% { transform: translateY(11px); }
  15% { transform: translateY(0); } 100% { transform: translateY(0); } }
@keyframes sbmTapRing { 0% { transform: scale(.45); opacity: 0; }
  6% { transform: scale(.5); opacity: .85; } 28% { transform: scale(1.55); opacity: 0; }
  100% { transform: scale(1.55); opacity: 0; } }
/* Hidden until the rail has been still for two seconds. Toggling display is
   deliberate: it restarts the keyframes, so the hand always arrives by
   tapping rather than fading in halfway through a cycle. */
.sbm-fing, .sbm-tapring { display: none; }
.sbm-strip.hint-on .sbm-card .sbm-fing,
.sbm-strip.hint-on .sbm-card .sbm-tapring { display: block; }
/* Retired everywhere the first time a card is actually tapped — one card
   taught is the whole lesson. Must out-weigh the rule above, not just
   follow it, and so must the turned-over case. */
.sbm-strip.taught .sbm-card .sbm-fing,
.sbm-strip.taught .sbm-card .sbm-tapring { display: none; }
/* Only the card in the middle gets the hint. Every card carries one so
   whichever you land on can teach you, but showing them all at once put a
   hand over every picture in the rail — the gesture is about the card you
   are looking at, not about all of them.

   Written as a HIDE rather than by narrowing the show rule above: the
   "taught" and "turned over" rules that follow are hides too, and narrowing
   the show rule would have out-weighed them, leaving the hint on screen
   after it had been learned. */
.sbm-strip .sbm-card:not(.is-centre) .sbm-fing,
.sbm-strip .sbm-card:not(.is-centre) .sbm-tapring { display: none; }
.sbm-strip .sbm-card.flipped .sbm-fing,
.sbm-strip .sbm-card.flipped .sbm-tapring { display: none; }
/* Once a card IS open, the next thing nobody would guess is that its back
   scrolls — so a second hand says so, only on the cards whose back actually
   overflows, and only until a back has been scrolled once. */
.sbm-scrollhint { left: 50%; bottom: 6px; margin-left: -26px; width: 52px; height: 60px;
  z-index: 4; display: none; animation: sbmScrollLoop 1.9s ease-in-out infinite; }
.sbm-scrollhint svg { width: 52px; height: 60px; }
.sbm-card.flipped.can-scroll .sbm-scrollhint { display: block; }
.sbm-strip.scroll-taught .sbm-card.flipped.can-scroll .sbm-scrollhint { display: none; }
@keyframes sbmScrollLoop {
  0% { transform: translateY(12px); opacity: 0; }
  16% { transform: translateY(12px); opacity: 1; }
  70% { transform: translateY(-16px); opacity: 1; }
  92% { transform: translateY(-24px); opacity: 0; }
  100% { transform: translateY(12px); opacity: 0; }
}

/* ===================================================================
   View: the level rail
   =================================================================== */
.sbm-lc {
  --card-h: 306px;
  flex: 0 0 calc(var(--card-h) * .755); height: var(--card-h);
  scroll-snap-align: center; scroll-snap-stop: always;
  border-radius: 14px; border: 2px solid var(--crimson); background: var(--bark);
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  font-family: 'Crimson Text', Georgia, serif; cursor: pointer; padding: 0; position: relative;
  -webkit-tap-highlight-color: transparent;
}
.sbm-lc .tg { font-size: 16px; letter-spacing: 5px; color: var(--gold-dim); text-transform: uppercase; }
.sbm-lc .nu { font-size: calc(var(--card-h) * .41); line-height: 1; color: var(--crimson); }
.sbm-lc .nmw { height: 52px; display: flex; align-items: center; justify-content: center;
  padding: 0 10px; margin-top: 6px; }
.sbm-lc .nm { font-size: 19px; letter-spacing: 1.5px; color: var(--ink);
  text-transform: uppercase; text-align: center; line-height: 1.2; }
/* Fixed-height zones: content varies, the layout doesn't. */
.sbm-ltitle { flex: 0 0 44px; display: flex; align-items: center; justify-content: center; padding: 0 20px; }
.sbm-ltitle b { font-size: 22px; letter-spacing: 2px; color: var(--crimson);
  text-transform: uppercase; font-weight: 400; }
.sbm-lcap { flex: 0 0 92px; display: flex; align-items: center; justify-content: center; padding: 0 20px; }
.sbm-lcap p { font-size: 19px; line-height: 1.45; color: var(--ink-mid); text-align: center; margin: 0; }
/* The level rail's swipe demo is driven (see mobile.js) rather than a CSS
   loop: it moves the rail itself, which no keyframe can do. */
.sbm-swipe { position: absolute; top: 50%; left: 50%; margin: -34px 0 0 -30px;
  width: 60px; height: 68px; pointer-events: none; opacity: 0; z-index: 5; }
.sbm-swipe svg { width: 60px; height: 68px; fill: #fff; stroke: #15100c; stroke-width: .9;
  stroke-linejoin: round; filter: drop-shadow(0 2px 5px rgba(0, 0, 0, .4)); }

/* ===================================================================
   View: the option list
   =================================================================== */
/* Search appears only when the list is long enough to need it: a
   permanently open field costs 62px of list on every long step to hold a
   control most people never touch. */
.sbm-searchwrap { flex: 0 0 auto; padding: 12px 20px 4px; display: none; }
.sbm.searching .sbm-searchwrap { display: block; }
.sbm-searchwrap input {
  width: 100%; height: 46px; border-radius: 10px; border: 1.5px solid var(--border);
  background: var(--parchment); color: var(--ink); font-family: 'Crimson Text', Georgia, serif;
  font-size: 17px; padding: 0 14px; outline: none;
}
.sbm-searchwrap input:focus { border-color: var(--crimson); }
.sbm-searchwrap input::placeholder { color: var(--gold-dim); }

.sbm-list { flex: 1; min-height: 0; overflow-y: auto; padding: 10px 20px 16px;
  -webkit-overflow-scrolling: touch; scrollbar-width: none; overscroll-behavior-y: contain; }
.sbm-list::-webkit-scrollbar { display: none; }
.sbm-row {
  width: 100%; text-align: left; display: flex; align-items: center; gap: 12px;
  background: var(--bark); border: 1.5px solid var(--border); border-radius: 12px;
  padding: 13px 14px; margin-bottom: 10px; cursor: pointer;
  font-family: 'Crimson Text', Georgia, serif; -webkit-tap-highlight-color: transparent;
  transition: background-color .16s ease, border-color .16s ease;
}
.sbm-row:last-child { margin-bottom: 0; }
.sbm-row-txt { flex: 1; min-width: 0; }
/* display:block is load-bearing: as inline spans the name and the blurb run
   together on one line, and the margin between them is silently inert. */
.sbm-row-name { display: block; font-size: calc(19px * var(--tscale, 1)); color: var(--ink); line-height: 1.2; }
.sbm-row-sub { display: block; font-size: calc(14px * var(--tscale, 1)); color: var(--gold-dim);
  line-height: 1.35; margin-top: 3px; }
/* The tick is always present, just empty — a box that appears only once
   ticked leaves you guessing whether the row was even selectable. */
.sbm-tick { flex: none; width: 26px; height: 26px; border-radius: 50%;
  border: 1.5px solid var(--border); display: flex; align-items: center; justify-content: center; }
.sbm-tick svg { width: 15px; height: 15px; stroke: var(--sbm-on-accent); fill: none;
  stroke-width: 3; stroke-linecap: round; stroke-linejoin: round; opacity: 0; }
.sbm-row.on { background: var(--crimson); border-color: var(--crimson); }
.sbm-row.on .sbm-row-name { color: var(--sbm-on-accent); }
.sbm-row.on .sbm-row-sub { color: var(--sbm-on-accent); opacity: .78; }
.sbm-row.on .sbm-tick { background: rgba(var(--shade-rgb), .18); border-color: var(--sbm-on-accent); }
.sbm-row.on .sbm-tick svg { opacity: 1; }
/* Quota filled, or already known from elsewhere: what you can't pick says
   so, rather than going silent. */
.sbm-row.blocked { opacity: .42; }
.sbm-row.known { opacity: .55; }
.sbm-known-badge { flex: none; font-family: system-ui, sans-serif; font-size: 10px;
  letter-spacing: 1px; text-transform: uppercase; color: var(--gold-dim);
  border: 1px solid var(--border); border-radius: 999px; padding: 2px 7px; }
/* The info tap target, kept apart from the row so choosing and reading are
   two different gestures rather than one guess. */
.sbm-row-i { flex: none; width: 34px; height: 34px; border-radius: 50%;
  border: 1.5px solid var(--border); background: transparent; color: var(--gold-dim);
  font-family: 'Crimson Text', Georgia, serif; font-size: 17px; font-style: italic;
  display: flex; align-items: center; justify-content: center; cursor: pointer; padding: 0; }
.sbm-row.on .sbm-row-i { border-color: var(--sbm-on-accent); color: var(--sbm-on-accent); }
.sbm-empty { text-align: center; color: var(--gold-dim); font-size: 17px; padding: 30px 10px; }

/* A "+ Create custom …" row closes the list, in the list's own idiom. */
.sbm-row.create { border-style: dashed; border-color: var(--crimson); background: transparent;
  color: var(--crimson); justify-content: center; gap: 10px; }
.sbm-row.create .sbm-row-name { color: var(--crimson); text-transform: uppercase; letter-spacing: 1.5px; }

/* ── the detail sheet ──────────────────────────────────────────────
   The long info text has nowhere to live in a row, and expanding a row in
   place shoves everything below it down mid-read. It slides up over the
   list instead, and the list keeps its scroll position underneath. */
.sbm-scrim { position: absolute; inset: 0; background: rgba(var(--shade-rgb), .5); z-index: 8;
  opacity: 0; pointer-events: none; transition: opacity .28s ease; }
.sbm-scrim.open { opacity: 1; pointer-events: auto; }
.sbm-sheet {
  position: absolute; left: 0; right: 0; bottom: 0; z-index: 9;
  background: var(--parchment); border-top: 2px solid var(--crimson);
  border-radius: 18px 18px 0 0;
  padding: 10px 22px calc(34px + env(safe-area-inset-bottom, 0px));
  max-height: 74%; display: flex; flex-direction: column;
  transform: translateY(101%); transition: transform .32s cubic-bezier(.3, .7, .3, 1);
}
.sbm-sheet.open { transform: translateY(0); }
.sbm-sheet-grab { width: 44px; height: 5px; border-radius: 3px; background: var(--border);
  margin: 0 auto 14px; flex: none; }
.sbm-sheet-name { font-family: 'Cinzel', Georgia, serif; font-size: 23px; font-weight: 700;
  color: var(--ink); flex: none; }
.sbm-sheet-kind { font-size: 13px; color: var(--gold-dim); font-style: italic; margin-top: 2px; flex: none; }
.sbm-sheet-body { overflow-y: auto; flex: 1; min-height: 0; margin-top: 14px; font-size: 17px;
  line-height: 1.55; color: var(--ink-mid); scrollbar-width: none; }
.sbm-sheet-body::-webkit-scrollbar { display: none; }
.sbm-sheet-body p { margin: 0 0 10px; }
.sbm-sheet-pick { margin-top: 16px; flex: none; }

/* ── "are you sure" ────────────────────────────────────────────────
   Only ever raised when something is genuinely being left behind, and it
   never refuses — the safe action is the loud one, carrying on is the
   quiet one, and both are one tap away. */
.sbm-mscrim { position: absolute; inset: 0; background: rgba(var(--shade-rgb), .55); z-index: 10;
  opacity: 0; pointer-events: none; transition: opacity .22s ease; }
.sbm-mscrim.open { opacity: 1; pointer-events: auto; }
.sbm-modal {
  position: absolute; left: 24px; right: 24px; top: 50%; z-index: 11;
  background: var(--parchment); border: 2px solid var(--crimson); border-radius: 16px;
  padding: 24px 22px 22px; text-align: center;
  transform: translateY(-50%) scale(.94); opacity: 0; pointer-events: none;
  transition: opacity .22s ease, transform .22s cubic-bezier(.3, .8, .4, 1);
}
.sbm-modal.open { opacity: 1; transform: translateY(-50%) scale(1); pointer-events: auto; }
.sbm-modal-ttl { font-family: 'Cinzel', Georgia, serif; font-size: 24px; font-weight: 700; color: var(--crimson); }
.sbm-modal-body { font-size: 17px; line-height: 1.5; color: var(--ink-mid); margin: 12px 0 20px; }
.sbm-modal-go { width: 100%; height: 50px; margin-top: 10px; background: transparent; border: none;
  color: var(--gold-dim); font-family: 'Crimson Text', Georgia, serif; font-size: 16px;
  letter-spacing: 1.5px; text-transform: uppercase; cursor: pointer; text-decoration: underline;
  text-underline-offset: 4px; -webkit-tap-highlight-color: transparent; }

/* ===================================================================
   View: ability scores
   =================================================================== */
.sbm-screen { flex: 1; min-height: 0; display: none; flex-direction: column; }
.sbm-screen.on { display: flex; }

.sbm-methods { flex: 1; min-height: 0; display: flex; flex-direction: column;
  justify-content: center; gap: 16px; padding: 0 24px; }
.sbm-method { width: 100%; background: var(--bark); border: 2px solid var(--crimson);
  border-radius: 14px; padding: 22px 20px; text-align: center; cursor: pointer;
  font-family: 'Crimson Text', Georgia, serif; -webkit-tap-highlight-color: transparent;
  transition: transform .12s ease, background-color .16s ease; }
.sbm-method:active { transform: scale(.98); background: var(--parchment); }
.sbm-method-ico { height: 56px; display: flex; align-items: center; justify-content: center; margin-bottom: 10px; }
.sbm-method-ico svg { width: 52px; height: 52px; }
/* The card is a <button>, so its span children are inline unless told
   otherwise — without display:block the title and the description run
   together on one line. */
.sbm-method-ttl { display: block; font-size: 24px; letter-spacing: 2px; color: var(--crimson); text-transform: uppercase; }
.sbm-method-sub { display: block; font-size: 15px; line-height: 1.45; color: var(--ink-mid); margin-top: 7px; }

.sbm-manual { flex: 1; min-height: 0; overflow-y: auto; padding: 14px 18px 8px; scrollbar-width: none; }
.sbm-manual::-webkit-scrollbar { display: none; }
.sbm-mrow { display: flex; align-items: center; gap: 10px; background: var(--bark);
  border: 1.5px solid var(--border); border-radius: 12px; padding: 9px 10px; margin-bottom: 9px; }
.sbm-mrow:last-child { margin-bottom: 0; }
.sbm-mrow-txt { flex: 1; min-width: 0; }
.sbm-mrow-name { display: block; font-size: 17px; color: var(--ink); line-height: 1.15; }
.sbm-mrow-desc { display: block; font-size: 12.5px; color: var(--gold-dim); line-height: 1.25; margin-top: 2px; }
.sbm-stepbtn { flex: none; width: 50px; height: 50px; border-radius: 10px;
  border: 1.5px solid var(--crimson); background: transparent; color: var(--crimson);
  display: flex; align-items: center; justify-content: center; cursor: pointer; padding: 0;
  -webkit-tap-highlight-color: transparent; }
.sbm-stepbtn:active { background: var(--crimson); color: var(--sbm-on-accent); }
.sbm-stepbtn svg { width: 20px; height: 20px; stroke: currentColor; stroke-width: 2.6;
  stroke-linecap: round; fill: none; }
.sbm-stepbtn[disabled] { opacity: .3; pointer-events: none; }
/* Typed entry stays available — the arrows are for nudging, the field for
   someone who already knows the number they want. */
.sbm-mrow-in { flex: none; width: 58px; height: 50px; border-radius: 10px;
  border: 1.5px solid var(--border); background: var(--parchment); color: var(--ink);
  font-family: 'Crimson Text', Georgia, serif; font-size: 23px; text-align: center;
  padding: 0; outline: none; -moz-appearance: textfield; }
.sbm-mrow-in::-webkit-outer-spin-button, .sbm-mrow-in::-webkit-inner-spin-button {
  -webkit-appearance: none; margin: 0; }
.sbm-mrow-in:focus { border-color: var(--crimson); }
.sbm-mrow-math { flex: none; min-width: 56px; text-align: right; }
.sbm-mrow-bonus { display: block; font-size: 11.5px; color: var(--crimson); line-height: 1.2; }
.sbm-mrow-total { display: block; font-size: 17px; color: var(--ink); line-height: 1.2; }
.sbm-mrow-mod { color: var(--gold-dim); font-size: 14px; }

.sbm-rollwrap { flex: 1; min-height: 0; display: flex; flex-direction: column;
  align-items: center; justify-content: center; padding: 0 24px; gap: 18px; }
/* A real icosahedron drawn on canvas — twenty shaded faces that tumble and
   settle on the face carrying the result. The flat SVG hexagon it replaces
   could only ever fake a die by swapping the number in the middle. */
.sbm-d20 { width: 168px; height: 168px; display: block; }
.sbm-d20.pop { animation: sbmDiePop .6s cubic-bezier(.2, 1.7, .4, 1); }
@keyframes sbmDiePop { 0% { transform: scale(1); } 32% { transform: scale(1.18); } 100% { transform: scale(1); } }
.sbm-rolled { display: flex; gap: 9px; justify-content: center; min-height: 52px; }
.sbm-rolled .r { width: 48px; height: 52px; border-radius: 10px; border: 1.5px dashed var(--border);
  display: flex; align-items: center; justify-content: center; font-size: 23px; color: var(--gold-dim); }
.sbm-rolled .r.set { border-style: solid; background: var(--bark); color: var(--ink);
  animation: sbmLand .38s cubic-bezier(.2, 1.5, .4, 1); }
@keyframes sbmLand { 0% { transform: scale(.4); opacity: 0; } 60% { transform: scale(1.14); }
  100% { transform: scale(1); opacity: 1; } }
.sbm-roll-warn { font-size: 14px; font-style: italic; color: var(--gold-dim); text-align: center;
  line-height: 1.45; margin: 0; max-width: 280px; }

/* user-select inherits: one declaration keeps a drag from turning into the
   browser selecting the numbers it passes over. */
.sbm-assign { flex: 1; min-height: 0; display: flex; gap: 10px; padding: 12px 16px 6px;
  user-select: none; -webkit-user-select: none; -webkit-touch-callout: none; }
.sbm-col { flex: 1; display: flex; flex-direction: column; justify-content: space-between; gap: var(--vgap, 8px); }
.sbm-col-vals { flex: 0 0 78px; }
.sbm-val { height: var(--cell, 52px); border-radius: 10px; border: 1.5px solid var(--crimson);
  background: var(--bark); color: var(--ink); font-family: 'Crimson Text', Georgia, serif;
  font-size: 23px; display: flex; align-items: center; justify-content: center;
  cursor: grab; touch-action: none; -webkit-tap-highlight-color: transparent;
  transition: opacity .18s ease, transform .18s cubic-bezier(.3, 1.4, .4, 1); }
.sbm-val.gone { opacity: 0; pointer-events: none; transform: scale(.4); }
/* Waiting numbers breathe on a stagger, so the column reads as live rather
   than as six parked boxes. It stops the moment one is picked up. */
.sbm-val:not(.gone) { animation: sbmIdle 2.6s ease-in-out infinite; }
.sbm-val:nth-child(2) { animation-delay: .18s; } .sbm-val:nth-child(3) { animation-delay: .36s; }
.sbm-val:nth-child(4) { animation-delay: .54s; } .sbm-val:nth-child(5) { animation-delay: .72s; }
.sbm-val:nth-child(6) { animation-delay: .9s; }
@keyframes sbmIdle { 0%, 100% { transform: translateY(0) rotate(0); }
  50% { transform: translateY(-3px) rotate(-1.2deg); } }
.sbm.dragging .sbm-val { animation: none; }
.sbm-val.lifted { opacity: .28; animation: none; }
/* While something is in hand every empty slot pulses, so where it can go is
   never a guess. */
.sbm.dragging .sbm-slot:not(.full) { animation: sbmBeckon 1.1s ease-in-out infinite; }
@keyframes sbmBeckon { 0%, 100% { border-color: var(--border); } 50% { border-color: var(--crimson); } }
/* Slots hug the right edge, the numbers the left, with the gap between them
   the thing you drag across. Ragged widths line up on their right-hand ends,
   which is also the end the numbers arrive at. */
.sbm-slots { align-items: flex-end; }
/* No width here — an empty slot is measured and given its own in JS, so it
   wraps its contents exactly and no two are the same width. A literal value
   would either pad short abilities with dead space or crush long ones. */
.sbm-slot { height: var(--cell, 52px); border-radius: 10px; border: 1.5px dashed var(--border);
  background: transparent; display: flex; align-items: center; padding: 0 12px; gap: 12px;
  position: relative;
  transition: width .34s cubic-bezier(.25, 1.15, .4, 1), transform .16s cubic-bezier(.3, 1.4, .5, 1),
              border-color .16s ease, background-color .16s ease; }
/* No width here either — filled slots are measured like empty ones, so the
   box is exactly as wide as what's in it. */
.sbm-slot.full { border-style: solid; border-color: var(--crimson); background: var(--bark); }
/* Shrink-wrapped in both states. A fixed width here is what put a lane of air
   between an ability and its number, and pushing that lane to the other end
   only moved the problem — so there is no lane. */
.sbm-slot-ab { flex: 0 1 auto; cursor: pointer; }
.sbm-slot-name { display: block; font-size: 15px; color: var(--ink); line-height: 1.1; white-space: nowrap; }
.sbm-slot-desc { display: block; font-size: 11px; color: var(--gold-dim); line-height: 1.15;
  margin-top: 1px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
/* The racial bonus as a notification badge: out of the row's flow entirely,
   pinned over the slot's top-left corner. Out of flow also means a slot with
   a bonus is no wider than one without, so the column stays even. */
.sbm-slot-bonus { position: absolute; top: -9px; left: -9px; z-index: 4;
  width: 28px; height: 28px; display: flex; align-items: center; justify-content: center;
  font-size: 15px; line-height: 1; color: var(--sbm-on-accent);
  background: var(--crimson); border: 1.5px solid var(--crimson-deep, var(--crimson));
  border-radius: 8px; box-shadow: 0 2px 6px rgba(var(--shade-rgb), .32); cursor: pointer;
  -webkit-tap-highlight-color: transparent; transform-origin: center; will-change: transform;
  animation: sbmWiggle 4.2s ease-in-out infinite; }
.sbm-slot-bonus:active { filter: brightness(.85); }
/* Idle for most of the cycle, then a short shake — an occasional nudge, not a
   thing jittering in your peripheral vision the whole time. Staggered so the
   badges never shake in unison. */
@keyframes sbmWiggle {
  0%, 86%, 100% { transform: rotate(0) scale(1); }
  88% { transform: rotate(-12deg) scale(1.12); }
  91% { transform: rotate(10deg) scale(1.12); }
  94% { transform: rotate(-7deg) scale(1.06); }
  97% { transform: rotate(3deg) scale(1); }
}
.sbm-slot:nth-child(2) .sbm-slot-bonus { animation-delay: .9s; }
.sbm-slot:nth-child(3) .sbm-slot-bonus { animation-delay: 1.9s; }
.sbm-slot:nth-child(4) .sbm-slot-bonus { animation-delay: 2.6s; }
.sbm-slot:nth-child(5) .sbm-slot-bonus { animation-delay: 1.3s; }
.sbm-slot:nth-child(6) .sbm-slot-bonus { animation-delay: 3.1s; }
/* The wiggle must stand down while the badge is being thrown at the number,
   or the two transforms fight over the same element. */
.sbm-slot.landing .sbm-slot-bonus { animation: none; }
.sbm-slot-val { flex: none; min-width: 46px; height: calc(var(--cell, 52px) - 12px);
  border-radius: 8px; display: flex; align-items: center; justify-content: center;
  font-size: 21px; color: var(--gold-dim); padding: 0 6px; transform-origin: center; }
.sbm-slot.full .sbm-slot-val { background: var(--crimson); color: var(--sbm-on-accent);
  cursor: grab; touch-action: none; }
/* An unlabelled "(+4)" beside an 18 reads as a second bonus being added,
   not as what the 18 is worth. Labelled, and tappable for the rest. */
.sbm-slot-mod { flex: none; display: flex; flex-direction: column; align-items: center;
  justify-content: center; min-width: 44px; line-height: 1; cursor: pointer;
  -webkit-tap-highlight-color: transparent; border-radius: 8px; padding: 3px 4px; }
.sbm-slot-mod:active { background: rgba(var(--accent-rgb), .12); }
.sbm-slot-mod b { font-size: 17px; font-weight: 400; color: var(--ink); }
.sbm-slot-mod i { font-family: system-ui, sans-serif; font-size: 8.5px; letter-spacing: 1.2px;
  text-transform: uppercase; color: var(--gold-dim); font-style: normal; margin-top: 2px; }
.sbm-slot.over { transform: scale(1.07); border-color: var(--crimson); background: var(--parchment); }

/* Where a bonus came from, on tap. */
.sbm-tip { position: absolute; z-index: 22; max-width: 250px; background: var(--parchment);
  border: 1.5px solid var(--crimson); border-radius: 10px; padding: 10px 13px;
  box-shadow: 0 6px 18px rgba(var(--shade-rgb), .34); opacity: 0; pointer-events: none;
  transform: translate(-50%, -100%) scale(.9); transform-origin: 50% 110%;
  transition: opacity .18s ease, transform .18s cubic-bezier(.2, 1.4, .4, 1); }
.sbm-tip.open { opacity: 1; pointer-events: auto; transform: translate(-50%, -100%) scale(1); }
.sbm-tip-ttl { font-family: 'Cinzel', Georgia, serif; font-size: 14px; font-weight: 700;
  color: var(--ink); margin-bottom: 5px; }
.sbm-tip-line { font-size: 14px; line-height: 1.4; color: var(--ink-mid); }
.sbm-tip-line b { color: var(--crimson); font-weight: 400; }
.sbm-tip::after { content: ''; position: absolute; left: 50%; bottom: -8px; width: 14px; height: 14px;
  margin-left: -7px; background: var(--parchment); border-right: 1.5px solid var(--crimson);
  border-bottom: 1.5px solid var(--crimson); transform: rotate(45deg); }

/* The "+2" that floats off after the number swallows the bonus, and the
   copy that follows your finger. */
.sbm-floater { position: absolute; z-index: 21; font-family: 'Crimson Text', Georgia, serif;
  font-size: 19px; color: var(--crimson); pointer-events: none; transform: translate(-50%, -50%); }
.sbm-ghost { position: absolute; z-index: 20; width: 48px; height: 48px; border-radius: 10px;
  border: 1.5px solid var(--crimson); background: var(--crimson); color: var(--sbm-on-accent);
  font-family: 'Crimson Text', Georgia, serif; font-size: 23px; display: flex;
  align-items: center; justify-content: center; pointer-events: none;
  transform: translate(-50%, -50%) scale(1.12); box-shadow: 0 6px 16px rgba(var(--shade-rgb), .4); }
.sbm-spark { position: absolute; width: 6px; height: 6px; border-radius: 50%;
  background: var(--crimson); pointer-events: none; z-index: 19; }
/* Crimson sparks vanish against the crimson die, so the dice throw
   parchment ones with a faint glow instead. */
.sbm-spark.gold { background: var(--sbm-on-accent); box-shadow: 0 0 6px rgba(var(--shade-rgb), .5); }

/* ===================================================================
   View: generic — step types with no bespoke phone design yet
   ===================================================================
   The desktop markup, hosted in the same shell and re-dressed by these
   rules: the same parchment ground, the same rows, the same ink button.
   A step type gains its own view by registering one (see mobile.js);
   until it does, it still arrives looking like the rest of the creator
   rather than like the desktop page shrunk down. */
.sbm-body { flex: 1; min-height: 0; overflow-y: auto; padding: 14px 18px 20px;
  -webkit-overflow-scrolling: touch; scrollbar-width: none; }
.sbm-body::-webkit-scrollbar { display: none; }

.sbm-body .step-card { background: transparent; border: none; box-shadow: none; padding: 0; }
/* The shell's hero already carries the title, the subtitle and the rules. */
.sbm-body .step-header-sticky { display: none; }
.sbm-body .option-grid, .sbm-body .option-grid.narrow {
  display: flex; flex-direction: column; gap: 10px; }
.sbm-body .option-card {
  background: var(--bark); border: 1.5px solid var(--border); border-radius: 12px;
  padding: 13px 14px; margin: 0; min-height: 0; text-align: left; }
.sbm-body .option-card.selected { background: var(--crimson); border-color: var(--crimson); }
.sbm-body .option-card.selected .option-card-label,
.sbm-body .option-card.selected .option-card-desc { color: var(--sbm-on-accent); }
.sbm-body .option-card-label { font-size: 19px; color: var(--ink); }
.sbm-body .option-card-desc { font-size: 14px; color: var(--gold-dim); }
.sbm-body .info-panel, .sbm-body .formula-box, .sbm-body .composite-substep {
  background: var(--bark); border: 1.5px solid var(--border); border-radius: 12px; }
/* Every confirm the hosted markup brings with it becomes the ink button —
   including the ones the shell never learns the name of. */
.sbm-body .btn-primary, .sbm-body .btn.btn-primary, .sbm-body .btn-big {
  width: 100%; max-width: none !important; height: 62px; border-radius: 10px;
  border: 1.5px solid var(--crimson); background: transparent !important; color: var(--crimson);
  font-family: 'Crimson Text', Georgia, serif; font-size: 19px; letter-spacing: 2.5px;
  text-transform: uppercase; box-shadow: none; }
.sbm-body .multi-confirm { flex-direction: column; gap: 8px; background: transparent;
  border: none; padding: 12px 0 0; position: static; }
.sbm-body .multi-count { color: var(--gold-dim); font-style: italic; }

/* ===================================================================
   View: composite — the parts of a kit
   ===================================================================
   One strip of slots showing every part of the choice at once; the list
   below belongs to whichever part is active. */
.sbm-parts { flex: 0 0 auto; display: flex; gap: 8px; padding: 10px 16px 8px; }
.sbm-part { flex: 1; min-width: 0; border-radius: 11px; border: 1.5px dashed var(--border);
  background: transparent; padding: 8px 9px; text-align: left; cursor: pointer;
  font-family: 'Crimson Text', Georgia, serif; -webkit-tap-highlight-color: transparent;
  position: relative; transition: border-color .16s ease, background-color .16s ease; }
.sbm-part-k { display: block; font-family: system-ui, sans-serif; font-size: 8.5px;
  letter-spacing: 1.2px; text-transform: uppercase; color: var(--gold-dim); }
.sbm-part-v { display: block; font-size: 14px; color: var(--ink-mid); line-height: 1.2;
  margin-top: 3px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.sbm-part.filled { border-style: solid; border-color: var(--border); background: var(--bark); }
.sbm-part.filled .sbm-part-v { color: var(--ink); }
.sbm-part.active { border-style: solid; border-color: var(--crimson); background: var(--bark); }
.sbm-part.active .sbm-part-k { color: var(--crimson); }
/* A tick on the parts already chosen, so what is left to do is countable at
   a glance rather than by reading. */
.sbm-part-tick { position: absolute; top: -7px; right: -7px; width: 20px; height: 20px;
  border-radius: 50%; background: var(--crimson); display: none; align-items: center;
  justify-content: center; }
.sbm-part.filled .sbm-part-tick { display: flex; }
.sbm-part-tick svg { width: 11px; height: 11px; stroke: var(--sbm-on-accent); fill: none;
  stroke-width: 3.4; stroke-linecap: round; stroke-linejoin: round; }
.sbm-given { flex: 0 0 auto; padding: 0 16px 4px; }
.sbm-given, .sbm-given p { font-size: 13px; line-height: 1.45; color: var(--gold-dim);
  font-style: italic; margin: 0; text-align: center; }
/* The search field only appears when armed from the hero, same as the list. */
.sbm[data-view="composite"] .sbm-searchwrap { display: none; }
.sbm[data-view="composite"].searching .sbm-searchwrap { display: block; }
.sbm[data-view="composite"] .sbm-find { display: none; }
.sbm[data-view="composite"].can-search .sbm-find { display: flex; }

/* ===================================================================
   View: identity — one question, the desktop's own form
   ===================================================================
   The hosted markup is renderTextInputForm's, restyled to fill the
   screen. Its own confirm button is hidden — the shell's held button
   fires the same confirm. */
.sbm-idbody { flex: 1; min-height: 0; overflow-y: auto; padding: 18px 20px 8px;
  -webkit-overflow-scrolling: touch; scrollbar-width: none; }
.sbm-idbody::-webkit-scrollbar { display: none; }
.sbm-idbody .btn-primary { display: none !important; }
.sbm-idbody .text-form-field { margin: 0 0 14px; position: relative; }
.sbm-idbody .field-explanation { font-size: 15px; line-height: 1.5; color: var(--ink-mid);
  margin: 0 0 12px; }
.sbm-idbody .field-input { width: 100%; border-radius: 12px; border: 1.5px solid var(--border);
  background: var(--parchment); color: var(--ink); font-family: 'Crimson Text', Georgia, serif;
  font-size: 19px; padding: 14px 15px; outline: none; }
.sbm-idbody .field-input:focus { border-color: var(--crimson); }
.sbm-idbody .field-textarea { min-height: 130px; line-height: 1.5; resize: none; }
.sbm-idbody .field-textarea-large { min-height: 200px; }
.sbm-idbody .char-counter { text-align: right; font-size: 12.5px; color: var(--gold-dim);
  margin-top: 5px; }
/* The die icon, phone-sized: a 44px target in the corner of the box, or
   inline in the height row. */
.sbm-idbody .field-input-wrap { position: relative; }
.sbm-idbody .field-input-wrap .field-input { padding-right: 54px; }
.sbm-idbody .field-random-btn { display: inline-flex; align-items: center; justify-content: center;
  width: 44px; height: 44px; padding: 0; margin: 0; border-radius: 50%;
  border: 1.5px solid var(--border); background: var(--parchment); font-size: 21px;
  line-height: 1; cursor: pointer; -webkit-tap-highlight-color: transparent; }
.sbm-idbody .field-input-wrap .field-random-btn { position: absolute; right: 8px; bottom: 8px; }
.sbm-idbody .height-input-row .field-random-btn { flex: 0 0 auto; }
.sbm-idbody .height-input-row { display: flex; align-items: center; gap: 9px; }
.sbm-idbody .height-part { width: 86px; text-align: center; }
.sbm-idbody .height-unit { font-size: 17px; color: var(--ink-mid); }
.sbm-idbody .height-info-btn { width: 40px; height: 40px; border-radius: 50%;
  border: 1.5px solid var(--border); background: transparent; color: var(--gold-dim);
  font-family: 'Crimson Text', Georgia, serif; font-style: italic; font-size: 18px;
  text-transform: none; cursor: pointer; }
.sbm-idbody .height-info-panel { margin-top: 12px; background: var(--bark);
  border: 1.5px solid var(--border); border-radius: 12px; padding: 12px 14px;
  font-size: 14px; line-height: 1.5; }
/* While the keyboard is up the banner gives way — the field has to stay
   reachable above the keys without the explanation pushing it off. */
.sbm.typing .sbm-hero-text { max-height: 12vh; }
.sbm.typing .sbm-pad { padding-top: 4px; padding-bottom: 10px; }
.sbm.typing .sbm-note { display: none; }

/* ===================================================================
   View: summary — the finished character sheet
   ===================================================================
   The desktop sheet in the shell, one tab of it at a time. Nothing here
   restyles what a section IS — the sheet's own stylesheet still draws
   every box — these rules only choose which sections show and give the
   type room a phone needs. */
.sbm-sum { flex: 1; min-height: 0; display: flex; flex-direction: column; }
.sbm-sumtabs { flex: 0 0 auto; display: flex; gap: 6px; padding: 10px 12px 8px;
  overflow-x: auto; scrollbar-width: none; }
.sbm-sumtabs::-webkit-scrollbar { display: none; }
.sbm-sumtab { flex: 0 0 auto; padding: 9px 15px; border-radius: 999px;
  border: 1.5px solid var(--border); background: transparent; color: var(--ink-mid);
  font-family: 'Crimson Text', Georgia, serif; font-size: 15.5px; cursor: pointer;
  -webkit-tap-highlight-color: transparent; }
.sbm-sumtab.on { background: var(--crimson); border-color: var(--crimson);
  color: var(--sbm-on-accent); }
/* The sheet is paper in every theme — --sheet-bg is parchment and --sheet-text
   is black whatever else the site is wearing. Stripping the background off it
   left black-and-crimson ink sitting on the shell's dark ground: readable in a
   light theme, nearly invisible in a dark one. The paper goes on the SCROLLER
   rather than on .character-sheet so it covers the spell and reference pages
   too — those are siblings of the sheet, not children of it, so the Spells tab
   was unpapered as well. */
.sbm-sumscroll { flex: 1; min-height: 0; overflow-y: auto; padding: 10px 12px 16px;
  background: var(--sheet-bg); color: var(--sheet-text);
  -webkit-overflow-scrolling: touch; scrollbar-width: none; }
.sbm-sumscroll::-webkit-scrollbar { display: none; }
.sbm-sumpad { padding-top: 6px; padding-bottom: max(12px, env(safe-area-inset-bottom, 0px)); }
/* Save takes three quarters of the row; the level-up arrow the last one. */
.sbm-sumpad-split { display: flex; gap: 10px; }
.sbm-sumpad-split .sbm-ink { width: auto; }
.sbm-sumpad-split #sbm-go { flex: 3 1 0; min-width: 0; }
.sbm-lvlup { flex: 1 1 0; min-width: 0; position: relative; overflow: hidden; padding: 0; }
.sbm-lvlup svg { width: 26px; height: 26px; display: block; margin: 0 auto;
  stroke: currentColor; }
/* The shine — a band of light sweeping the button every few seconds. */
.sbm-lvlup::after { content: ''; position: absolute; top: -45%; bottom: -45%;
  width: 36%; left: -60%; transform: rotate(18deg); pointer-events: none;
  background: linear-gradient(90deg, transparent, rgba(255, 244, 214, .6), transparent);
  animation: sbmLvlShine 3.4s ease-in-out infinite; }
@keyframes sbmLvlShine { 0%, 58% { left: -60%; } 88%, 100% { left: 130%; } }
@media (prefers-reduced-motion: reduce) { .sbm-lvlup::after { animation: none; } }

/* Screen only: this is the phone's reading of the sheet — the toolbar's
   jobs moved into the ⋯ menu, the frame relaxes so the phone column owns
   the width, one tab shows at a time and the type comes up a size. Paper
   wants the sheet the print rules in styles.css are written against:
   whole, framed, and at its own size. */
@media screen {
  .sbm-sum .summary-toolbar { display: none !important; }
  .sbm-sum .character-sheet { max-width: none; border: none; box-shadow: none;
    background: transparent; color: var(--sheet-text); padding: 0; overflow: visible; }

  /* One tab at a time. display only — nothing moves in the DOM, so the
     positional edit-restore and every delegated handler keep their footing. */
  .sbm-sum:not([data-tab="stats"]) .sbm-t-stats { display: none !important; }
  .sbm-sum:not([data-tab="skills"]) .sbm-t-skills { display: none !important; }
  .sbm-sum:not([data-tab="combat"]) .sbm-t-combat { display: none !important; }
  .sbm-sum:not([data-tab="gear"]) .sbm-t-gear { display: none !important; }
  .sbm-sum:not([data-tab="features"]) .sbm-t-features { display: none !important; }
  .sbm-sum:not([data-tab="spells"]) .sbm-t-spells { display: none !important; }
  .sbm-sum:not([data-tab="details"]) .sbm-t-details { display: none !important; }

  /* Room to read. The sheet is typeset for paper at 12px; a phone held in a
     hand gets the same content a size up. */
  .sbm-sum .character-sheet { font-size: 15px; line-height: 1.45; }
  .sbm-sum .cs-skill-row { font-size: 15px; padding: 3px 0; }
  .sbm-sum .cs-stat-row { font-size: 15px; }
  .sbm-sum .cs-attack-row, .sbm-sum .cs-attack-header { font-size: 14.5px; }
  .sbm-sum .cs-equip-list, .sbm-sum .cs-bag-list { font-size: 15px; line-height: 1.5; }
  .sbm-sum .cs-features-text, .sbm-sum .cs-personality-text { font-size: 15px; line-height: 1.5; }
  .sbm-sum .cs-prof-item, .sbm-sum .cs-prof-subsection { font-size: 15px; }
  .sbm-sum .cs-field-label { font-size: 10px; }
  .sbm-sum .cs-combat-val { font-size: 24px; }
  .sbm-sum .cs-box-title { font-size: 15px; }
  .sbm-sum .cs-secondary-pages { font-size: 15px; }
}

/* ── the shell is a screen, not a page ─────────────────────────────────
   The browser's own Print (Ctrl+P, the menu) prints the LIVE page, and
   on the phone summary that page is a fixed, clipped, one-tab-at-a-time
   viewport — so what came out was the banner, the tab strip and whichever
   slice of the sheet happened to be scrolled into view, followed by blank
   pages. The ⋯ menu's Print never had this problem (it lays the sheet out
   in a tab of its own), but the browser's Print has to work too.

   On paper the shell steps aside: its chrome stays off, its bands stop
   pinning and clipping, and the sheet inside is left to the print rules,
   which take it from there. */
@media print {
  .sbm-hero, .sbm-sumtabs, .sbm-pad, .sbm-summenu, .sbm-summenu-scrim,
  .sbm-printset { display: none !important; }
  .sbm { position: static !important; top: auto !important; z-index: auto !important;
    height: auto !important; max-height: none !important; overflow: visible !important;
    display: block !important; background: transparent !important; }
  .sbm-sum, .sbm-sumscroll { display: block !important; flex: none !important;
    height: auto !important; max-height: none !important; min-height: 0 !important;
    overflow: visible !important; padding: 0 !important; background: transparent !important; }
}

/* The ⋯ menu. */
.sbm-kebab { right: 12px; top: 50%; margin-top: -22px; }
.sbm-summenu-scrim { position: fixed; inset: 0; z-index: 48; display: none; }
.sbm-summenu-scrim.open { display: block; }
.sbm-summenu { position: fixed; top: calc(var(--sb-topbar-h, 0px) + 66px); right: 12px;
  z-index: 49; min-width: 230px; background: var(--parchment);
  border: 1.5px solid var(--crimson); border-radius: 12px; overflow: hidden;
  box-shadow: 0 10px 30px rgba(var(--shade-rgb), .4);
  opacity: 0; pointer-events: none; transform: translateY(-6px) scale(.98);
  transform-origin: top right; transition: opacity .16s ease, transform .16s ease; }
.sbm-summenu.open { opacity: 1; pointer-events: auto; transform: none; }
.sbm-summenu button { display: block; width: 100%; text-align: left; padding: 14px 18px;
  border: none; border-bottom: 1px solid var(--border); background: transparent;
  color: var(--ink); font-family: 'Crimson Text', Georgia, serif; font-size: 17px;
  cursor: pointer; -webkit-tap-highlight-color: transparent; }
.sbm-summenu button:last-child { border-bottom: none; }
.sbm-summenu button:active { background: var(--bark); }

/* The full sheet: the desktop page as paper.
   It starts BELOW the site's own top bar — same rule the blown card follows,
   so the navbar stays on top and reachable instead of eating the first inch
   of the sheet and the way out. The bar of its own is what makes leaving
   obvious: a labelled Back, in the phone shell's colours, always in view
   because it sits outside the part that scrolls. */
.sbm-fso { position: fixed; left: 0; right: 0; bottom: 0; top: var(--sb-topbar-h, 0px);
  z-index: 8995; background: #2a231c; display: flex; flex-direction: column; }
.sbm-fso-bar { flex: none; position: relative; display: flex; align-items: center;
  gap: 8px; padding: 8px 12px; background: var(--crimson); color: var(--sbm-on-accent);
  box-shadow: 0 2px 10px rgba(0, 0, 0, .35); z-index: 2; }
.sbm-fso-back { display: flex; align-items: center; gap: 6px; padding: 10px 14px 10px 10px;
  border-radius: 10px; border: 1.5px solid rgba(var(--shade-rgb), .3);
  background: transparent; color: var(--sbm-on-accent); cursor: pointer;
  font-family: 'Crimson Text', Georgia, serif; font-size: 16px; line-height: 1;
  -webkit-tap-highlight-color: transparent; }
.sbm-fso-back:active { background: rgba(var(--shade-rgb), .22); }
.sbm-fso-back svg { width: 20px; height: 20px; stroke: currentColor; fill: none;
  stroke-width: 2; stroke-linecap: round; stroke-linejoin: round; }
/* Centred on the BAR, not on what's left of it, so the title doesn't shift
   with the width of the button beside it. */
.sbm-fso-ttl { position: absolute; left: 0; right: 0; text-align: center;
  padding: 0 100px; pointer-events: none;
  font-family: 'Cinzel', Georgia, serif; font-size: 14px; letter-spacing: 1.5px;
  text-transform: uppercase; color: var(--sbm-on-accent); white-space: nowrap;
  overflow: hidden; text-overflow: ellipsis; }
.sbm-fso-scroll { position: relative; flex: 1; min-height: 0; overflow: auto;
  -webkit-overflow-scrolling: touch; touch-action: pan-x pan-y; }
.sbm-fso-wrap { position: relative; overflow: hidden; }
.sbm-fso-paper { border: 0; display: block; transform-origin: 0 0;
  background: var(--parchment); box-shadow: 0 6px 26px rgba(0, 0, 0, .45); }
.sbm-fso-hint { position: absolute; bottom: 22px; left: 50%; transform: translateX(-50%);
  z-index: 3; padding: 8px 16px; border-radius: 999px; background: rgba(21, 16, 12, .78);
  color: #F0E5D0; font-family: 'Crimson Text', Georgia, serif; font-size: 14px;
  transition: opacity .5s ease; pointer-events: none; }
.sbm-fso-hint.gone { opacity: 0; pointer-events: none; }

/* The interstitial splash a section heading opens with. */
.sbm-splash { flex: 1; min-height: 0; display: flex; flex-direction: column;
  align-items: center; justify-content: center; gap: 14px; padding: 0 28px; text-align: center; }
.sbm-splash h1 { font-family: 'Cinzel', Georgia, serif; font-size: 30px; font-weight: 700;
  letter-spacing: 2px; color: var(--crimson); margin: 0; text-transform: uppercase; }
.sbm-splash .sbm-splash-rule { width: 90px; height: 2px;
  background: linear-gradient(to right, transparent, var(--border), transparent); }
.sbm-splash p { font-size: 21.5px; line-height: 1.55; color: var(--ink-mid); margin: 0; }

@media (prefers-reduced-motion: reduce) {
  .sbm-fing, .sbm-tapring, .sbm-scrollhint, .sbm-val:not(.gone), .sbm-slot-bonus,
  .sbm-d20.pop, .sbm.dragging .sbm-slot:not(.full) { animation: none !important; }
  .sbm-flip, .sbm-sheet, .sbm-modal, .sbm-slot, .sbm-val { transition: none !important; }
}

/* The assign screen's shortcut. Quiet on purpose: dragging the six where you
   want them is the point of the screen, and this is the way out for someone
   who does not care to. Hidden the moment every score has a home. */
.sbm-autowrap { flex: 0 0 auto; text-align: center; padding: 0 20px 2px; }
.sbm-auto { background: none; border: none; padding: 4px 8px;
  color: var(--gold-dim); font-family: 'Cinzel', Georgia, serif; font-size: 12.5px;
  font-weight: 700; letter-spacing: .4px; text-decoration: underline;
  text-underline-offset: 3px; cursor: pointer;
  -webkit-tap-highlight-color: transparent; }
.sbm-auto:active { color: var(--crimson); }
.sbm-auto[hidden] { display: none; }

/* An item's picture on its row — a square thumbnail leading the name. Only
   present when the catalogue actually has art for that item, so a shelf
   that's only half illustrated has no gaps in it. */
/* Portrait, and contained rather than cropped: item art is 2:3, and a
   square crop takes the ends off a sword — the two features that tell a
   greatsword from a dagger at a glance. */
.sbm-row-art { flex: 0 0 auto; width: 42px; height: 58px; border-radius: 8px;
  border: 1px solid var(--border); background: var(--parchment) center/contain no-repeat;
  margin-right: 2px; }
.sbm-row.on .sbm-row-art { border-color: var(--sbm-on-accent); }

/* ── the composite's card rail ──────────────────────────────────────
   A part whose options are illustrated is browsed rather than read, so it
   borrows the race rail wholesale — same cards, same halo, same dots and
   caption. It sits under the parts strip, which is why it takes its height
   from the room it is given rather than from the rail view's own budget. */
.sbm-cprail { flex: 1; min-height: 0; display: flex; flex-direction: column; }
.sbm-cprail[hidden] { display: none; }
.sbm-cprail .sbm-dots { flex: 0 0 auto; padding: 2px 0 6px; }
.sbm-cprail .sbm-caption { flex: 0 0 auto; }
/* The one already taken wears the tick the list rows wear. */
.sbm-cprail .sbm-card.picked .sbm-face { border-color: var(--crimson); box-shadow: inset 0 0 0 3px var(--crimson); }
.sbm-cprail .sbm-card.known { opacity: .4; pointer-events: none; }

/* ── a card grows as it turns ────────────────────────────────────────
   The back is a page of text on a card sized for a picture, so at rest it
   had to be set small enough to fit — which is the wrong size to read. It
   grows on the way over and the type comes up with it. Transform only, so
   the rail's scroll-snap positions never move; the rails leave headroom for
   it (see the fit functions) so the taller card is not clipped. */
.sbm-card { transition: transform .55s cubic-bezier(.4, .05, .2, 1); }
.sbm-card.flipped { z-index: 6; }
.sbm-card.flipped .sbm-cardback { font-size: 1.06em; }
.sbm-card.flipped .sbm-info-name { font-size: 21px; }
.sbm-card.flipped .sbm-info-kind { font-size: 13px; }
.sbm-card.flipped .sbm-info-scroll { font-size: 14.5px; line-height: 1.5; }
.sbm-card.flipped .sbm-fact-k { font-size: 10px; }
.sbm-card.flipped .sbm-fact-v { font-size: 14px; }
.sbm-card.flipped .sbm-item-text { font-size: 14.5px; line-height: 1.55; }
@media (prefers-reduced-motion: reduce) { .sbm-card { transition: none !important; } }

/* ── a card turned over, full size ───────────────────────────────────
   Lifted out of the rail (which clips) and animated from where it sat to
   most of the screen. Under the navbar's z-index so the site's own chrome
   stays on top and reachable. */
.sbm-cardscrim { position: fixed; inset: 0; z-index: 8990; background: rgba(0, 0, 0, .55);
  opacity: 0; pointer-events: none; transition: opacity .4s ease; }
.sbm-cardscrim.on { opacity: 1; pointer-events: auto; }
/* Sized by the browser, not by arithmetic: left/right/top/bottom pin it to
   the viewport with an even margin, so it is centred whatever the page zoom
   is doing. Under the navbar (9000+) and over everything else. */
.sbm-card.blown { position: fixed; z-index: 8995; margin: 0;
  left: 14px; right: 14px;
  top: calc(var(--sb-topbar-h, 0px) + 14px); bottom: 14px;
  width: auto; height: auto; flex: none;
  scroll-snap-align: none; touch-action: auto;
  animation: sbmBlowIn .55s cubic-bezier(.4, .05, .2, 1); }
/* Grows AND turns, together. The scale rides on the CARD while the rotation
   rides on .sbm-flip INSIDE it — two elements, so the two transforms never
   touch each other and can run at once. Same duration and easing as the flip
   (see .sbm-flip) so they read as one movement rather than two in a queue.
   Doing them in sequence is what made it feel clunky. */
@keyframes sbmBlowIn { from { opacity: .3; transform: scale(var(--blow-from, .5)); }
  to { opacity: 1; transform: none; } }
/* And back down on the way out, while it turns face-up again. */
.sbm-card.blown.closing { animation: sbmBlowOut .5s cubic-bezier(.4, .05, .2, 1) forwards; }
/* Ends at the card's ACTUAL size — --blow-from is measured when it opens
   (see _sbmBlowUp). A fixed scale here was a guess, and it guessed far too
   small, so the card vanished to a speck before reappearing in the rail. */
@keyframes sbmBlowOut { from { opacity: 1; transform: none; }
  to { opacity: .4;
    transform: translate(var(--blow-dx, 0), var(--blow-dy, 0)) scale(var(--blow-from, .5)); } }
/* At this size the back is a page, so it is set like one. */
.sbm-card.blown .sbm-info-name { font-size: 27px; }
.sbm-card.blown .sbm-info-kind { font-size: 15px; }
.sbm-card.blown .sbm-info-head { padding: 20px 22px 0; }
.sbm-card.blown .sbm-info-scroll { font-size: 17px; line-height: 1.6; padding: 16px 22px 22px; }
.sbm-card.blown .sbm-info-tag { font-size: 10px; padding: 3px 8px; }
.sbm-card.blown .sbm-fact-k { font-size: 11px; }
.sbm-card.blown .sbm-fact-v { font-size: 16px; }
.sbm-card.blown .sbm-item-text { font-size: 17px; line-height: 1.6; }
.sbm-card.blown .sbm-info-subs-label { font-size: 11px; }
.sbm-card.blown .sbm-info-chip { font-size: 14px; }
/* The picture behind it has no business bleeding across the whole screen. */
.sbm-card.blown .sbm-bleed { display: none; }
@media (prefers-reduced-motion: reduce) {
  .sbm-cardscrim, .sbm-card.blown { transition: none !important; }
}
  left: 14px;
  top: calc(var(--sb-topbar-h, 0px) + 14px);
  /* Divided by --sb-ui-scale: the page runs at zoom .8, so 100vw is the
     screen width expressed in units that then get scaled DOWN again — it lands
     at 80% of the screen. Same idiom the rest of the sheet uses (see body and
     .split-right in styles.css). */
  width: calc(100vw / var(--sb-ui-scale, 1) - 28px);
  height: calc(100vh / var(--sb-ui-scale, 1) - var(--sb-topbar-h, 0px) - 28px);
}


/* The grow-in-place scale would fight the geometry being written here. */

.sbm-card.blown .sbm-bleed { display: none; }
.sbm-card.blown .sbm-face, .sbm-card.blown .sbm-cardback { border-radius: 16px; }
/* The back becomes a page: a fixed head and a body that scrolls. */
.sbm-card.blown .sbm-cardback { display: flex; flex-direction: column; overflow: hidden; }
.sbm-card.blown .sbm-info-head { flex: 0 0 auto; padding: 18px 20px 0; }
.sbm-card.blown .sbm-info-name { font-size: 27px; }
.sbm-card.blown .sbm-info-kind { font-size: 15px; }
.sbm-card.blown .sbm-info-scroll { flex: 1 1 auto; min-height: 0; overflow-y: auto;
  -webkit-overflow-scrolling: touch; touch-action: pan-y; overscroll-behavior: contain;
  padding: 14px 20px 22px; font-size: 16.5px; line-height: 1.6; }
.sbm-card.blown .sbm-fact-k { font-size: 11px; }
.sbm-card.blown .sbm-fact-v, .sbm-card.blown .sbm-item-text { font-size: 16px; line-height: 1.6; }
.sbm-card.blown .sbm-info-tag { font-size: 10px; }
/* The hand that says "tap me" has done its job by now. */
/* The tap hand has done its job by the time the card is open. */
.sbm-card.blown .sbm-fing, .sbm-card.blown .sbm-tapring { display: none; }
/* The scroll hand is different: on an opened card the back IS the thing that
   scrolls, so it is worth offering — but only after a few seconds of nobody
   moving, and never once they have shown they know (see _sbmArmScrollHint). */
.sbm-card.blown .sbm-scrollhint { display: none; }
.sbm-card.blown.hint-scroll .sbm-scrollhint { display: block; }
body.sbm-scroll-taught .sbm-scrollhint { display: none !important; }
@media (prefers-reduced-motion: reduce) { .sbm-card.blown { transition: none !important; } }

/* One answer to give: the row IS the choice, so it carries no tick box. A
   chosen row is already unmistakable — it turns crimson. */
.sbm-list.single-pick .sbm-tick { display: none; }

/* The close button on an opened card. Outside the flipper, so it stays facing
   you while the card turns; above it, so it is always the thing you hit. */
.sbm-blow-x { display: none; position: absolute; top: 10px; right: 10px; z-index: 7;
  width: 40px; height: 40px; border-radius: 50%;
  border: 1.5px solid var(--crimson); background: var(--parchment);
  color: var(--crimson); font-size: 18px; line-height: 1; cursor: pointer;
  align-items: center; justify-content: center;
  box-shadow: 0 2px 8px rgba(var(--shade-rgb), .3);
  -webkit-tap-highlight-color: transparent; }
.sbm-card.blown .sbm-blow-x { display: flex; }
.sbm-blow-x:active { background: var(--crimson); color: var(--sbm-on-accent); }
/* Room for it, so a long name does not run under the button. */
.sbm-card.blown .sbm-info-head { padding-right: 58px; }

/* The gap a card leaves while it is open. Same class as a card so it is the
   same size without anything being measured; invisible and untouchable. */
.sbm-card.sbm-hole { visibility: hidden; pointer-events: none; }

/* ── print settings ──────────────────────────────────────────────────
   Asks the two things the desktop toolbar asks — paper and how much
   feature text — and offers only what fits, which the hidden frame works
   out for itself (see _sbmPrintSheet). */
.sbm-printset { position: fixed; inset: 0; z-index: 8996; }
.sbm-printset-scrim { position: absolute; inset: 0; background: rgba(var(--shade-rgb), .6); }
.sbm-printset-box { position: absolute; left: 50%; top: 50%; transform: translate(-50%, -50%);
  width: min(340px, calc(100vw - 40px)); background: var(--parchment);
  border: 1.5px solid var(--crimson); border-radius: 14px; padding: 18px 18px 14px;
  box-shadow: 0 12px 34px rgba(var(--shade-rgb), .45); }
.sbm-printset-ttl { font-family: 'Cinzel', Georgia, serif; font-size: 19px; font-weight: 700;
  color: var(--crimson); text-align: center; margin-bottom: 12px; }
.sbm-printset-load { font-size: 15px; color: var(--ink-mid); font-style: italic;
  text-align: center; padding: 10px 0 14px; }
.sbm-printset-label { font-family: 'Cinzel', Georgia, serif; font-size: 10.5px;
  letter-spacing: 1.2px; text-transform: uppercase; color: var(--gold-dim); margin: 10px 0 6px; }
.sbm-printset-row { display: flex; gap: 8px; }
.sbm-printset-row button { flex: 1; min-width: 0; padding: 11px 6px; border-radius: 9px;
  border: 1.5px solid var(--border); background: transparent; color: var(--ink);
  font-family: 'Crimson Text', Georgia, serif; font-size: 15px; cursor: pointer;
  -webkit-tap-highlight-color: transparent; }
.sbm-printset-row button.on { border-color: var(--crimson); background: var(--crimson);
  color: var(--sbm-on-accent); }
/* Greyed rather than removed: seeing that a level exists but will not fit is
   the useful half of the answer. */
.sbm-printset-row button[disabled] { opacity: .32; cursor: default; }
.sbm-printset-note { font-size: 13px; line-height: 1.45; color: var(--gold-dim);
  font-style: italic; margin: 12px 0 0; }
.sbm-printset-btns { display: flex; gap: 8px; margin-top: 16px; }
.sbm-printset-btns button, .sbm-printset-btns a { flex: 1; padding: 13px 8px; border-radius: 10px;
  display: inline-flex; align-items: center; justify-content: center; text-decoration: none; box-sizing: border-box;
  font-family: 'Crimson Text', Georgia, serif; font-size: 16px; cursor: pointer;
  -webkit-tap-highlight-color: transparent; }
.sbm-printset-cancel { border: 1.5px solid var(--border); background: transparent; color: var(--ink-mid); }
.sbm-printset-go { border: 1.5px solid var(--crimson); background: var(--crimson); color: var(--sbm-on-accent); }
.sbm-printset-go[disabled], .sbm-printset-go[aria-disabled="true"] { opacity: .45; pointer-events: none; }
.sbm-printset-build { margin: 10px 0 0; font-size: 11px; letter-spacing: .4px; color: var(--gold-dim); opacity: .7; text-align: right; }
/* Laid out at desktop width but off-screen: a frame that is not laid out
   cannot measure what fits, and measuring is the point of it. */
.sbm-printset-frame { position: fixed; left: -10000px; top: 0; width: 1200px; height: 1600px; border: 0; }

/* Print frame: 1200px so it sits clear of the phone shell's own
   (max-width:1024px) range — at 1000 it was inside it, and the page it drew
   was not quite the desktop one. */
