/* ===================================================================
   Shared library card grid
   ===================================================================
   ONE definition of the "library card" look, used by every library page
   that presents its items as art cards (Characters, Monsters, …). Pages
   own their data, actions and popup copy; this file owns the
   presentation, so the libraries cannot drift apart visually — restyle
   here and every card library follows.

   Expected markup:
     .lib-toolbar > .lib-create-btn + .lib-search + .lib-tool-btn* + .lib-count
     .lib-grid    > .lib-card*
     .lib-card    > .lib-art  ( .lib-fallback, .lib-img-btn, .lib-actions )
                  + .lib-meta ( .lib-name, .lib-sub )

   Colours come from the page's theme variables (--bark, --crimson, …)
   with the dark palette as a fallback, so a page that forgets to define
   them still renders correctly.
   =================================================================== */

/* ── Toolbar ─────────────────────────────────────────────────────── */
.lib-toolbar{display:flex;align-items:center;gap:12px;margin-bottom:20px;flex-wrap:wrap;}
.lib-search{flex:1;min-width:220px;padding:10px 14px;background:var(--bark);border:1px solid var(--border);border-radius:8px;color:var(--ink);font-family:"Crimson Text",Georgia,serif;font-size:15px;}
.lib-search:focus{border-color:var(--crimson);outline:none;}
/* ── Filter and sort, inside the search box ──
   The same pair the creature creator's side panel carries, so a library
   is filtered the same way wherever you meet one. They sit ON the input's
   right-hand end rather than beside it: they belong to the search, and a
   toolbar row of four separate buttons reads as four unrelated things. */
.lib-search-wrap{flex:1;min-width:220px;position:relative;display:flex;}
.lib-search-wrap .lib-search{flex:1;min-width:0;padding-right:74px;}
.lib-tool-ico{position:absolute;top:50%;transform:translateY(-50%);width:28px;height:28px;padding:0;
  display:flex;align-items:center;justify-content:center;background:none;border:none;border-radius:6px;
  color:var(--gold-dim);cursor:pointer;transition:color .15s,background .15s;}
.lib-tool-ico:hover{color:var(--ink);background:rgba(var(--sheen-rgb),.07);}
.lib-tool-ico.is-on{color:var(--crimson);}
.lib-tool-ico svg{width:16px;height:16px;display:block;fill:none;stroke:currentColor;stroke-width:1.9;
  stroke-linecap:round;stroke-linejoin:round;}
.lib-tool-ico.f{right:38px;}
.lib-tool-ico.s{right:8px;}
/* Nothing showing because of a filter is a different thing from an empty
   library, and the difference is worth a line. */
.lib-filter-note{color:var(--gold-dim);font-size:13px;font-style:italic;}
.lib-filter-note button{background:none;border:none;color:var(--crimson);font:inherit;font-style:normal;
  cursor:pointer;text-decoration:underline;padding:0 0 0 4px;}
.lib-count{color:var(--gold-dim);font-size:13px;font-style:italic;white-space:nowrap;}
.lib-create-btn{display:inline-flex;align-items:center;gap:6px;padding:10px 20px;border:2px dashed var(--border);border-radius:8px;background:transparent;color:var(--gold-dim);font-family:'Cinzel',serif;font-size:12px;font-weight:600;letter-spacing:1px;text-decoration:none;cursor:pointer;transition:all .2s;}
.lib-create-btn:hover{border-color:var(--crimson);color:var(--crimson-hover);background:rgba(var(--accent-rgb),.05);}
.lib-create-btn span{font-size:16px;line-height:1;}
/* Secondary toolbar actions (Multi Select, Import JSON, …). Matches the
   accordion libraries' toolbar buttons so the two families stay familiar. */
.lib-tool-btn{padding:7px 14px;border-radius:5px;border:1px solid var(--border);background:transparent;color:var(--ink-mid);font-family:'Cinzel',serif;font-size:10px;font-weight:600;letter-spacing:.5px;text-transform:uppercase;cursor:pointer;transition:all .2s;white-space:nowrap;}
.lib-tool-btn:hover{border-color:var(--crimson);color:var(--text-strong);background:rgba(var(--accent-rgb),.08);}
.lib-tool-btn.active{background:var(--crimson);color:var(--text-on-crimson);border-color:var(--crimson);}

/* ── Card grid ───────────────────────────────────────────────────── */
.lib-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(220px,1fr));gap:20px;}
.lib-card{position:relative;aspect-ratio:3/4;border:1px solid var(--border);border-radius:12px;overflow:hidden;background:var(--bark);cursor:pointer;transition:border-color .2s,box-shadow .25s,transform .15s;box-shadow:0 2px 10px rgba(var(--shade-rgb),.35);}
/* Grows toward you on hover. The two curves are deliberately different: it
   eases OUT on the way in so it arrives softly, and eases IN on the way back so
   it lets go gently instead of snapping flat. Opacity rides the same timing so
   a dimmed card brightens as it lifts. */
.lib-card:hover{border-color:var(--crimson);box-shadow:0 14px 34px rgba(var(--accent-rgb),.26);transform:translateY(-6px) scale(1.035);z-index:1;}
/* "New …" tile — the first card in the grid. Matches a real card's footprint
   so the grid stays even, but reads as an empty slot waiting to be filled. */
.lib-create-card{display:flex;align-items:center;justify-content:center;text-align:center;aspect-ratio:3/4;border:2px dashed var(--border);border-radius:12px;background:transparent;box-shadow:none;color:var(--gold-dim);font-family:'Cinzel',serif;font-size:13px;font-weight:600;letter-spacing:1px;text-decoration:none;cursor:pointer;transition:all .2s;min-height:100%;padding:20px;}
.lib-create-card:hover{border-color:var(--crimson);color:var(--crimson-hover);background:rgba(var(--accent-rgb),.05);transform:translateY(-6px) scale(1.035);box-shadow:none;}
/* Fills the card rather than setting its height: the name bar overlays the
   bottom of this, so the art has to run the whole way down or the bar simply
   eats part of the picture instead of sitting over it. */
.lib-art{position:absolute;inset:0;width:100%;height:100%;background:linear-gradient(160deg,var(--bark-light) 0%,var(--bark-deep) 100%);background-size:cover;background-position:center 20%;}
.lib-fallback{position:absolute;inset:0;display:flex;align-items:center;justify-content:center;color:var(--gold-dim);opacity:.5;}
.lib-art.has-img .lib-fallback{display:none;}
.lib-art.has-img::after{content:"";position:absolute;inset:0;background:linear-gradient(to top,var(--media-veil),transparent 45%);pointer-events:none;}
.lib-icon{width:32px;height:32px;display:flex;align-items:center;justify-content:center;border:none;border-radius:50%;background:var(--media-scrim);color:var(--text-on-media);cursor:pointer;transition:background .15s,color .15s,transform .12s;backdrop-filter:blur(2px);}
.lib-icon:hover{background:var(--media-scrim-strong);transform:scale(1.08);}
.lib-icon svg{width:16px;height:16px;stroke:currentColor;fill:none;stroke-width:2;stroke-linecap:round;stroke-linejoin:round;}
.lib-icon.lib-danger:hover{background:var(--accent-strong);color: var(--text-on-accent);}
.lib-img-btn{position:absolute;top:8px;left:8px;z-index:2;opacity:0;transition:opacity .15s;}
.lib-actions{position:absolute;top:8px;right:8px;display:flex;gap:6px;z-index:2;opacity:0;transition:opacity .15s;}
.lib-card:hover .lib-img-btn,.lib-card:hover .lib-actions{opacity:1;}
/* Touch devices have no hover — keep the controls visible there. */
@media (hover:none){.lib-img-btn,.lib-actions{opacity:1;}}
/* The name bar sits OVER the artwork rather than under it, so the picture
   fills the whole card and you can still see it behind the text. 75% of the
   panel colour, blurred a little, so the name stays readable over whatever is
   underneath. color-mix rather than an rgba() token because no theme defines
   --bark as channels; the flat colour above it is the fallback. */
.lib-meta{position:absolute;left:0;right:0;bottom:0;padding:11px 13px 13px;background:var(--bark);background:color-mix(in srgb, var(--bark) 75%, transparent);backdrop-filter:blur(3px);-webkit-backdrop-filter:blur(3px);}
.lib-name{font-family:"Cinzel",serif;font-size:16px;font-weight:700;color:var(--ink);letter-spacing:.3px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;}
.lib-sub{margin-top:3px;font-size:13px;color:var(--gold-dim);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;}
.lib-empty{text-align:center;color:var(--gold-dim);font-style:italic;padding:60px 0;}
.lib-empty .ico{font-size:42px;margin-bottom:10px;opacity:.6;}

/* ── Waiting on AI artwork ────────────────────────────────────────
   Generation runs for the best part of a minute with the dialog already
   closed, so the card itself has to say it's working — otherwise nothing on
   screen suggests anything is happening. Lives here rather than on a page so
   every grid that can generate a picture (the world editor's content cards,
   the Campaign Settings covers) waits the same way. */
.lib-card.is-busy{pointer-events:none;}
.lib-card.is-busy .lib-art::after{content:"";position:absolute;inset:0;background:var(--media-scrim-strong);z-index:3;}
.lib-card.is-busy .lib-art::before{content:"";position:absolute;left:50%;top:50%;width:34px;height:34px;margin:-17px 0 0 -17px;z-index:4;border:3px solid var(--media-line);border-top-color:var(--text-on-media);border-radius:50%;animation:sbArtSpin .8s linear infinite;}
@keyframes sbArtSpin{to{transform:rotate(360deg);}}
.lib-card.is-busy .lib-meta::after{content:"Generating…";display:block;margin-top:5px;font-family:Cinzel,serif;font-size:9px;letter-spacing:1.2px;text-transform:uppercase;color:var(--crimson);}

/* ── Kind / source filter row ─────────────────────────────────────
   A centred segmented control under the search bar, used by every library
   that splits its contents (Creatures vs NPCs, SRD vs Custom). Larger than
   the toolbar buttons because it's the main way you move between views. */
.lib-kind-filter{display:flex;gap:0;justify-content:center;margin:0 0 20px;}
.lib-kind-filter .lib-tool-btn{border-radius:0;margin-left:-1px;font-size:14px;letter-spacing:1px;padding:11px 26px;}
.lib-kind-filter .lib-tool-btn:first-child{border-radius:6px 0 0 6px;margin-left:0;}
.lib-kind-filter .lib-tool-btn:last-child{border-radius:0 6px 6px 0;}
/* Corner tag on a card — marks an NPC, an SRD entry, GM-shared content, … */
/* Lifted clear of the name bar, which now overlays the bottom of the art. */
.lib-kind-badge{position:absolute;left:8px;bottom:66px;z-index:2;background:var(--crimson);color:var(--text-on-crimson);font-family:'Cinzel',serif;font-size:9px;font-weight:700;letter-spacing:.8px;padding:2px 7px;border-radius:999px;box-shadow:0 2px 6px rgba(var(--shade-rgb),.5);}
/* Shipped SRD content, not a kind of creature — so it wears the same pill in a
   quieter colour. Crimson is the badge that means "this one is different"; a
   library where two thirds of the cards shout it would say nothing at all. */
.lib-srd-badge{background:var(--media-scrim);color:var(--text-on-media);border:1px solid rgba(var(--gold-metal-rgb),.55);}

/* ── Drag to reorder (see shared/card-reorder.js) ──────────────────
   Cards are for clicking and dragging, not for reading off: leaving them
   selectable means every reorder ends in a swathe of blue highlight, which
   then swallows the start of the next drag. While a card is in hand the
   pointer ranges over the whole page, so the ban has to widen for the
   duration or the drag paints a selection across everything it crosses. */
.lib-grid{user-select:none;-webkit-user-select:none;}
body.sb-card-dragging{user-select:none;-webkit-user-select:none;}
/* The card in hand: lifted, following the cursor with no transition (one
   here would lag behind the pointer). pointer-events:none so the hit test
   under the cursor finds the card BENEATH it, not the card being held. */
.lib-card.dragging{transition:none;z-index:20;cursor:grabbing;pointer-events:none;box-shadow:0 18px 44px rgba(var(--shade-rgb),.6);opacity:.92;}
/* The rest glide aside. The hover lift would fight that mid-flight. */
.lib-grid.is-dragging .lib-card{cursor:grabbing;}
.lib-grid.is-dragging .lib-card:hover{transform:none;box-shadow:0 2px 10px rgba(var(--shade-rgb),.35);}
.lib-grid .lib-card{transition:transform .34s cubic-bezier(.25,.8,.3,1),box-shadow .34s cubic-bezier(.25,.8,.3,1),opacity .28s cubic-bezier(.25,.8,.3,1),border-color .2s;}
.lib-grid .lib-card:hover{transition:transform .26s cubic-bezier(.2,.65,.3,1),box-shadow .26s cubic-bezier(.2,.65,.3,1),opacity .2s cubic-bezier(.2,.65,.3,1),border-color .2s;}
/* The create tile lifts the same way so the row reads as one set. */
.lib-create-card{transition:transform .34s cubic-bezier(.25,.8,.3,1),border-color .2s,color .2s,background .2s;}
.lib-create-card:hover{transition:transform .26s cubic-bezier(.2,.65,.3,1),border-color .2s,color .2s,background .2s;}
@media (prefers-reduced-motion:reduce){
  .lib-grid .lib-card{transition:border-color .2s,box-shadow .25s;}
}

/* ── Dealing (see shared/card-deal.js) ────────────────────────────
   A grid full of cards arrives as a hand being dealt out, one card at a time
   from a deck sitting off the bottom-right corner of the screen, rather than
   simply being there. Every card flies the same journey — its own offset to
   that one point is measured in JS and arrives here as --dx/--dy, and the
   stagger comes in as its animation-delay. Nothing fades: a dealt card is
   solid the whole way, it's just off screen until it isn't. */
.lib-grid.dealing > .lib-card{animation:sbDealIn .4s cubic-bezier(.2,.85,.3,1) backwards;}
/* Marked in JS for cards sitting outside the viewport. Animating them is work
   with nothing to show for it, and on a long shelf that work is most of what
   the deal costs. They just take their place. */
.lib-grid > .lib-card.no-deal{animation:none !important;}
/* The "New …" tile just fades — it belongs to the grid rather than the hand. */
.lib-grid.dealing > .lib-create-card{animation:sbDealTileIn .26s ease backwards;animation-delay:60ms;}
@keyframes sbDealTileIn{from{opacity:0;}to{opacity:1;}}
@keyframes sbDealIn{
  from{transform:translate(var(--dx,420px),var(--dy,420px)) rotate(13deg) scale(.94);}
  to{transform:none;}
}
/* Leaving is the deal run backwards: the same corner, the last card down
   picked up first. */
.lib-grid.dealing-out > .lib-card{animation:sbDealOut .28s cubic-bezier(.45,0,.85,.4) forwards;}
.lib-grid.dealing-out > .lib-create-card{animation:sbDealTileOut .2s ease forwards;}
@keyframes sbDealTileOut{from{opacity:1;}to{opacity:0;}}
@keyframes sbDealOut{
  from{transform:none;}
  to{transform:translate(var(--dx,420px),var(--dy,420px)) rotate(13deg) scale(.94);}
}
/* Cards start well outside the page, which would otherwise stretch the
   scrollable area and flash the scrollbars for the length of the deal. A page
   marks the box to clip to with data-deal-clip and JS lights it for the
   flight; a card is then hidden until it has crossed onto the table. */
[data-deal-clip].sb-dealing{overflow:clip;}
@media (prefers-reduced-motion:reduce){
  .lib-grid.dealing > *,.lib-grid.dealing-out > *{animation:none;}
}

/* ── Selection (ctrl/shift-click — see shared/library-select.js) ──── */
/* .lib-grid.is-selecting is set whenever at least one card is selected. The
   per-card action icons stay available: the bin is what deletes the whole
   selection, so hiding it would take away the obvious way to act on one. */
.lib-grid.is-selecting .lib-card{cursor:pointer;}
.lib-card.is-selected{border-color:var(--crimson);box-shadow:0 0 0 2px var(--crimson),0 8px 24px rgba(var(--accent-rgb),.25);}
.lib-check{position:absolute;top:8px;right:8px;z-index:3;width:26px;height:26px;border-radius:50%;border:2px solid var(--media-line);background:var(--media-veil);display:none;align-items:center;justify-content:center;backdrop-filter:blur(2px);}
.lib-grid.is-selecting .lib-check{display:flex;}
.lib-card.is-selected .lib-check{background:var(--crimson);border-color:var(--crimson);}
.lib-check svg{width:15px;height:15px;stroke:var(--text-on-media);fill:none;stroke-width:3;stroke-linecap:round;stroke-linejoin:round;opacity:0;}
.lib-card.is-selected .lib-check svg{opacity:1;}

/* ── Popups (image menu, AI generation, action menus) ────────────── */
.lib-pop-overlay{display:none;position:fixed;inset:0;background:rgba(var(--shade-rgb),.7);z-index:10000;align-items:center;justify-content:center;padding:20px;}
.lib-pop-overlay.open{display:flex;}
.lib-pop-box{background:var(--bark);border:1px solid var(--border);border-top:3px solid var(--crimson);border-radius:12px;max-width:420px;width:100%;padding:22px;box-shadow:0 12px 48px rgba(var(--shade-rgb),.6);}
.lib-pop-title{font-family:"Cinzel",serif;font-size:17px;font-weight:700;color:var(--ink);letter-spacing:.5px;margin-bottom:4px;}
.lib-pop-sub{font-size:13px;color:var(--gold-dim);margin-bottom:16px;line-height:1.45;}
.lib-opt{display:flex;align-items:center;gap:12px;width:100%;text-align:left;background:var(--bark-dark);border:1px solid var(--border);border-radius:8px;padding:12px 14px;margin-bottom:8px;cursor:pointer;color:var(--ink);font-family:"Crimson Text",Georgia,serif;font-size:15px;transition:border-color .15s,background .15s;}
.lib-opt:hover{border-color:var(--crimson);background:rgba(var(--accent-rgb),.08);}
.lib-opt .oico{font-size:18px;width:22px;text-align:center;}
.lib-opt.danger:hover{border-color:var(--accent-strong);background:rgba(185,28,28,.12);}
.lib-pop-actions{display:flex;justify-content:flex-end;gap:10px;margin-top:12px;}
.lib-ta{width:100%;min-height:74px;padding:10px 12px;background:var(--bark-dark);border:1px solid var(--border);border-radius:8px;color:var(--ink);font-family:"Crimson Text",Georgia,serif;font-size:14px;resize:vertical;}
.lib-ta:focus{border-color:var(--crimson);outline:none;}
.lib-style-label{font-family:'Cinzel',serif;font-size:11px;letter-spacing:1px;text-transform:uppercase;color:var(--gold-dim);margin:14px 0 6px;}
.lib-styles{display:flex;gap:8px;}
.lib-style{flex:1;background:var(--bark-dark);border:1px solid var(--border);border-radius:6px;padding:9px 6px;cursor:pointer;font-family:'Cinzel',serif;font-size:12px;font-weight:700;color:var(--gold-dim);transition:all .15s;}
.lib-style:hover{border-color:var(--crimson);}
.lib-style.is-on{border-color:var(--crimson);background:rgba(var(--accent-rgb),.12);color:var(--text-strong);}
.lib-ai-loading{display:none;align-items:center;gap:8px;color:var(--gold-dim);font-size:13px;margin-top:12px;}
.lib-ai-loading.show{display:flex;}
.lib-spin{width:16px;height:16px;border:2px solid var(--border);border-top-color:var(--crimson);border-radius:50%;animation:lib-spin 1s linear infinite;}
@keyframes lib-spin{to{transform:rotate(360deg);}}
.lib-ai-error{color:var(--accent-bright);font-size:13px;margin-top:10px;min-height:1px;}

/* ── The ⋯ menu a card drops ─────────────────────────────────────────
   Anchored under the button that opened it, not a dialog in the middle of
   the screen: these actions belong to one card, and a full popup covers the
   grid you were reading. Positioned as fixed, so the coordinates
   shared/card-menu.js writes are viewport ones. That file owns the
   behaviour; this owns the look. */
.card-menu{position:fixed;z-index:100002;min-width:186px;background:var(--bark);border:1px solid var(--border);border-radius:9px;box-shadow:0 10px 30px rgba(var(--shade-rgb),.55);padding:5px;font-family:"Crimson Text",Georgia,serif;}
.card-menu-item{display:flex;align-items:center;gap:9px;width:100%;text-align:left;padding:8px 10px;background:none;border:none;border-radius:6px;color:var(--ink);font-family:inherit;font-size:13px;cursor:pointer;transition:background .12s,color .12s;}
.card-menu-item:hover{background:rgba(var(--accent-rgb),.12);color:var(--text-strong);}
.card-menu-item[disabled]{opacity:.4;cursor:default;background:none;color:var(--ink);}
.card-menu-item.is-danger{color:var(--accent-soft);}
.card-menu-item.is-danger:hover{background:var(--crimson);color:var(--text-on-crimson);}
.card-menu-ico{flex:0 0 auto;width:18px;text-align:center;font-size:15px;line-height:1;}
.card-menu-sep{height:1px;background:var(--border);margin:5px 3px;}

/* ── Opening a card ────────────────────────────────────────
   The plate flies out of the grid, the panel slides out of its side. The move
   itself is shared/card-popout.js; this is only the look. Every library that
   opens a card uses these, so a creature, a character and a world all open the
   same way. */
.info-overlay{position:fixed;inset:0;background:rgba(var(--shade-rgb),0);z-index:10000;display:none;transition:background .28s ease;}
.info-overlay.open{display:block;}
.info-overlay.lit{background:rgba(var(--shade-rgb),.72);}
.info-art{position:fixed;left:0;top:0;width:0;height:0;border-radius:12px;border:1px solid var(--border);
  background:var(--bark-dark) center/cover no-repeat;box-shadow:0 24px 70px rgba(var(--shade-rgb),.7);
  z-index:3;pointer-events:none;opacity:0;}
.info-art.flying{transition:left .33s cubic-bezier(.22,.61,.24,1),top .33s cubic-bezier(.22,.61,.24,1),
  width .33s cubic-bezier(.22,.61,.24,1),height .33s cubic-bezier(.22,.61,.24,1),opacity .16s ease;}
.info-art-glyph{position:absolute;inset:0;display:flex;align-items:center;justify-content:center;overflow:hidden;}
.info-box{position:fixed;left:0;top:0;width:0;height:0;background:var(--bark);border:1px solid var(--border);
  border-top:3px solid var(--crimson);border-radius:0 12px 12px 0;overflow:hidden;
  box-shadow:0 16px 56px rgba(var(--shade-rgb),.65);z-index:2;opacity:0;}
.info-box.sliding{transition:left .28s cubic-bezier(.22,.61,.24,1),width .28s cubic-bezier(.22,.61,.24,1),opacity .18s ease;}
/* The panel's contents keep the width they were dealt while it slides, so the
   text doesn't reflow on every frame of the opening. */
.info-box > .info-body{width:var(--panel-w,420px);height:100%;box-sizing:border-box;
  display:flex;flex-direction:column;min-height:0;overflow:hidden;padding:20px 24px 22px;position:relative;}
.info-box.sheet{left:50%;top:50%;transform:translate(-50%,-50%);width:min(560px,calc(100vw - 36px));
  height:auto;max-height:calc(100vh - 56px);overflow:auto;opacity:1;border-radius:12px;transition:none;}
.info-box.sheet .info-body{width:auto;}
.info-close{position:absolute;top:12px;right:14px;z-index:2;width:30px;height:30px;display:flex;
  align-items:center;justify-content:center;border:none;border-radius:50%;background:transparent;
  color:var(--gold-dim);font-size:15px;line-height:1;cursor:pointer;transition:color .15s,background .15s;}
.info-close:hover{color:var(--text-strong);background:rgba(var(--sheen-rgb),.08);}
.info-head{display:flex;align-items:baseline;gap:5px 10px;flex-wrap:wrap;padding-right:34px;}
.info-name{font-family:"Cinzel",serif;font-size:22px;font-weight:700;color:var(--text-strong);}
.info-kind{font-size:13px;color:var(--gold-dim);font-style:italic;margin-top:2px;}
.info-scroll{overflow-y:auto;margin:14px -6px 0;padding:0 6px;flex:1;min-height:0;}
.info-actions{display:flex;justify-content:flex-end;gap:10px;margin-top:20px;flex-wrap:wrap;}

/* ── A panel that IS the record ────────────────────────────────
   Some records already have a finished, canonical way of being read: a
   creature IS its stat block, name, type line, rules and all. Framed in a
   panel that printed the name again above it and left a margin around it,
   the sheet came out as a picture of a sheet in a box — and the sheet, the
   part you opened the card to read, got the smallest share of the panel.

   `is-full` hands the whole box over to it. The record runs corner to
   corner and scrolls inside, and all that is left of the panel is the
   close button riding over it and a bar of actions along the foot. The bar
   stays IN FLOW rather than floating: the world editor can put four
   buttons on it, and a floating bar would then cover the reading it is
   attached to.

   Every wrapper between the scroll and the sheet is made a stretching
   column, so a short creature still fills the panel — the stat block's own
   layout is built for it (.bottom-bar sits on margin-top:auto). */
.info-box > .info-body.is-full{padding:0;}
.info-box > .info-body.is-full > .info-scroll{margin:0;padding:0;}
.info-box > .info-body.is-full > .info-scroll,
.info-box > .info-body.is-full > .info-scroll > *,
.info-box > .info-body.is-full .sb-sheet-pair,
.info-box > .info-body.is-full .sb-sheet{display:flex;flex-direction:column;}
.info-box > .info-body.is-full > .info-scroll > *,
.info-box > .info-body.is-full .sb-sheet-pair,
.info-box > .info-body.is-full .sb-sheet,
.info-box > .info-body.is-full .sb-sheet > *,
.info-box > .info-body.is-full .statblock{flex:1 0 auto;}
.info-box > .info-body.is-full > .info-actions{margin:0;padding:10px 14px;gap:8px;
  background:var(--bark);border-top:1px solid var(--border);flex:0 0 auto;}
/* Over parchment instead of the panel's own dark, a bare glyph has nothing
   to sit on — so here it gets a disc. */
.info-body.is-full .info-close{background:rgba(var(--shade-rgb),.5);color:var(--parchment);}
.info-body.is-full .info-close:hover{background:rgba(var(--shade-rgb),.82);color:var(--parchment);}

@media (prefers-reduced-motion:reduce){
  .info-art.flying,.info-box.sliding,.info-overlay{transition:none;}
}

/* ── Phones ───────────────────────────────────────────────────────
   The desktop grid asks for 220px columns, which on a phone comes out as
   ONE card per row: a 3/4 plate as wide as the screen and taller than half
   of it, so a shelf of four settings is four screens of scrolling. Here the
   cards go two-up and everything drawn on them shrinks to match — the name
   bar, the action buttons, the badge — so a card stays a card rather than a
   poster.

   Sizes in this block are LAYOUT pixels, not the pixels you see: the app runs
   at :root{zoom:.8}, so a 150px column measures 120px on the glass. The query
   itself is matched against the real device width (375 on a phone), which the
   zoom does not touch — that mismatch is why the numbers here look large. */
@media (max-width: 640px) {
  .lib-grid{grid-template-columns:repeat(auto-fill,minmax(150px,1fr));gap:14px;}
  /* The "New …" tile stops being a card. Two-up it would sit in the place of
     a real one for no reason; as a bar across the top it's the button it
     always was, and the shelf proper starts on the first row. */
  .lib-create-card{grid-column:1/-1;aspect-ratio:auto;min-height:0;padding:15px 16px;font-size:12px;}
  .lib-meta{padding:8px 9px 9px;}
  /* Two lines rather than one clipped one — at this width a world called
     "The Sundered Reaches" would otherwise be "The Sundered…". */
  /* min-height reserves the second line whether or not the name needs it, so
     every bar on the shelf is the same height — and so the badge below has a
     fixed edge to clear instead of one that moves with the length of a name. */
  .lib-name{font-size:15px;line-height:1.15;min-height:35px;white-space:normal;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden;}
  .lib-sub{font-size:12px;line-height:1.2;margin-top:2px;}
  .lib-icon{width:28px;height:28px;}
  .lib-icon svg{width:14px;height:14px;}
  .lib-img-btn{top:6px;left:6px;}
  .lib-actions{top:6px;right:6px;gap:5px;}
  .lib-check{top:6px;right:6px;width:23px;height:23px;}
  .lib-check svg{width:13px;height:13px;}
  /* Sits just clear of the name bar: 8 + 35 + 2 + 14 + 9 of bar, then a gap. */
  .lib-kind-badge{left:6px;bottom:75px;font-size:8px;padding:2px 6px;}
  .lib-toolbar{gap:8px;margin-bottom:14px;}
  .lib-search{min-width:0;font-size:14px;padding:9px 12px;}
  .lib-kind-filter{margin-bottom:14px;}
  .lib-kind-filter .lib-tool-btn{padding:10px 0;font-size:12px;flex:1;text-align:center;}
  .lib-empty{padding:36px 0;}
}

/* No pointer, no lift. A tap that grows the card by 3.5% only shoves the rest
   of the shelf around under the finger, and on touch the :hover state sticks
   after the tap is over — so the card would stay lifted until you touched
   something else. Keyed off the input, not the width: a small window on a
   desktop still has a mouse. Last in the file so it beats the lift above. */
@media (hover:none) {
  .lib-card:hover{transform:none;box-shadow:0 2px 10px rgba(var(--shade-rgb),.35);}
  .lib-create-card:hover{transform:none;}
}

/* ── Edit mode: the shelf comes loose (see shared/card-reorder.js) ──
   Hold a card with a finger and every card on the shelf starts to wiggle,
   the way a phone home screen does when an icon comes loose. It says two
   things at once that nothing else on the page can say: these are movable,
   and you are moving them NOW — which is what makes it safe for a tap to
   rearrange rather than open.

   The wiggle is `rotate`, NOT a rotation inside `transform`. The drag writes
   a transform onto these same cards every frame to open the gap, and an
   animated transform beats an inline one — the shelf would freeze mid-drag.
   The independent rotate property composes with transform instead of
   replacing it, so the two can run at once. */
@keyframes sbCardJiggle{
  0%  { rotate: -.75deg; }
  50% { rotate:  .75deg; }
  100%{ rotate: -.75deg; }
}
/* .sb-in-frame is put on by the observer in shared/card-reorder.js: only the
   cards on screen carry the animation, so a 322-card shelf costs what the
   handful you can see costs. */
.lib-grid.is-jiggling > .lib-card.sb-in-frame{
  animation: sbCardJiggle .3s ease-in-out infinite;
  /* A finger on a card moves the card; the page is scrolled from the
     background, or by carrying a card to the top or bottom edge. */
  touch-action: none;
}
/* Staggered, so the shelf shivers instead of marching in step. */
.lib-grid.is-jiggling > .lib-card.sb-in-frame:nth-child(2n){animation-duration:.34s;animation-delay:-.12s;}
.lib-grid.is-jiggling > .lib-card.sb-in-frame:nth-child(3n){animation-duration:.27s;animation-delay:-.19s;}
.lib-grid.is-jiggling > .lib-card.sb-in-frame:nth-child(5n){animation-duration:.32s;animation-delay:-.07s;}
/* The card in hand holds still — it's being carried, not waiting to be. */
.lib-grid.is-jiggling > .lib-card.dragging{animation:none;rotate:0deg;}
/* The "New …" tile isn't part of the hand and can't be dealt into it. */
.lib-grid.is-jiggling > .lib-create-card{opacity:.4;pointer-events:none;}

/* The way out, for anyone who doesn't guess that tapping the background is
   one. Above the bottom nav where that exists, near the bottom edge where it
   doesn't. */
.sb-reorder-done{
  position:fixed;left:50%;bottom:24px;z-index:9500;
  transform:translate(-50%,12px);opacity:0;pointer-events:none;
  transition:opacity .18s ease,transform .18s ease;
  padding:11px 30px;border:1px solid var(--crimson);border-radius:999px;
  background:var(--bark-deep);color:var(--text-strong);
  font-family:'Cinzel',serif;font-size:13px;font-weight:700;letter-spacing:1.5px;
  text-transform:uppercase;cursor:pointer;
  box-shadow:0 8px 26px rgba(var(--shade-rgb),.6);
}
.sb-reorder-done.show{opacity:1;transform:translate(-50%,0);pointer-events:auto;}
@media (max-width:1024px){
  .sb-reorder-done{bottom:calc(70px + env(safe-area-inset-bottom,0px));}
}
@media (prefers-reduced-motion:reduce){
  .lib-grid.is-jiggling > .lib-card{animation:none;}
  /* Still says "loose", without the movement. */
  .lib-grid.is-jiggling > .lib-card{outline:2px dashed var(--crimson);outline-offset:-2px;}
}

/* ── An opened card on a phone: two faces of one card ──────────────
   The desktop move — a plate in the middle with a panel unfurling out of its
   side — needs a screen wide enough to hold both. A phone isn't, and the
   arithmetic said so: the panel came out at a negative width and the plate
   ran off the edge. Here the same two elements become the two FACES of one
   card, and opening it turns it over: the picture on the front, everything
   the panel said on the back. Same idea as the character creator's cards.

   No wrapper to rotate, because the markup is a plate and a panel side by
   side and always has been. Each face carries its own perspective() and its
   own rotation, half a turn apart, around the same centre — which is the
   same picture as a flipper, without a third element in the middle of it.
   The turn itself is written from shared/card-popout.js. */
.info-overlay.is-flip .info-art,
.info-overlay.is-flip .info-box{
  transform-origin:50% 50%;
  backface-visibility:hidden;-webkit-backface-visibility:hidden;
  border-radius:16px;
  transition:transform var(--sb-flip-ms,520ms) cubic-bezier(.4,.05,.2,1),opacity .3s ease;
  will-change:transform;
}
/* Not the side panel's shape: nothing is joined to its left edge any more. */
.info-overlay.is-flip .info-box{
  border:1px solid var(--border);border-top:3px solid var(--crimson);
  border-radius:16px;
}
/* The back is a page — a head that stays put and a body that scrolls. */
.info-overlay.is-flip .info-box > .info-body{
  width:100%;height:100%;padding:18px 16px 16px;
}
/* …unless the record IS the page, in which case it fills the turned card
   the same way it fills the side panel. */
.info-overlay.is-flip .info-box > .info-body.is-full{padding:0;}
.info-overlay.is-flip .info-scroll{-webkit-overflow-scrolling:touch;overscroll-behavior:contain;touch-action:pan-y;}
/* Room for the close button, so a long name doesn't run under it. */
.info-overlay.is-flip .info-head{padding-right:40px;}
.info-overlay.is-flip .info-close{width:36px;height:36px;font-size:17px;top:9px;right:10px;}
.info-overlay.is-flip .info-name{font-size:24px;}
.info-overlay.is-flip .info-actions{margin-top:14px;gap:8px;}
.info-overlay.is-flip .info-actions .btn{flex:1 1 auto;}
/* Turning it back to look at the picture. The button rides on the back
   beside the page's own close button; the front turns back wherever you
   touch it, and says so, because a full-bleed picture offers no other clue.
   The art only takes taps while it is the side facing you. */
.info-overlay.is-flip .info-art{pointer-events:none;}
.info-overlay.is-flip.show-front .info-art{pointer-events:auto;cursor:pointer;}
.info-turn{position:absolute;top:9px;left:10px;z-index:3;width:36px;height:36px;
  display:flex;align-items:center;justify-content:center;border:none;border-radius:50%;
  background:transparent;color:var(--gold-dim);font-size:19px;line-height:1;cursor:pointer;
  transition:color .15s,background .15s;}
.info-turn:hover,.info-turn:active{color:var(--text-strong);background:rgba(var(--sheen-rgb),.08);}
/* The head has the close button on one side and this on the other. */
.info-overlay.is-flip .info-head{padding-left:34px;}
.info-turn-hint{position:absolute;left:0;right:0;bottom:16px;z-index:2;text-align:center;
  font-family:'Cinzel',serif;font-size:11px;font-weight:600;letter-spacing:1.4px;
  text-transform:uppercase;color:var(--text-on-media);pointer-events:none;
  text-shadow:0 2px 8px rgba(var(--shade-rgb),.85);}
/* Only ever drawn on a turned-over card. */
.info-overlay:not(.is-flip) .info-turn,
.info-overlay:not(.is-flip) .info-turn-hint{display:none;}

@media (prefers-reduced-motion:reduce){
  .info-overlay.is-flip .info-art,.info-overlay.is-flip .info-box{transition:none;}
}

/* A sheet (a form or a dialog, with no card behind it) on a phone. 100vw is
   the screen measured in units the page zoom then shrinks again, so it has
   to be divided back out or the box lands at 80% of the width it asked for
   — see --sb-ui-scale in shared/navbar.css. */
@media (max-width:740px){
  .info-overlay .info-box.sheet{
    width:calc(100vw / var(--sb-ui-scale,1) - 28px);
    max-height:calc(100vh / var(--sb-ui-scale,1) - var(--sb-topbar-h,64px) - 28px);
  }
  .lib-pop-box{max-width:none;width:100%;}
}
