/* Roar — dark violet-ink, one signature: the holographic foil gradient,
   reserved for the wordmark and hit cards. Everything else stays quiet. */

:root {
  --ink: #14121f;          /* page background — deep violet-ink, night-stream */
  --surface: #1e1b2e;      /* cards, panels */
  --surface-2: #262239;    /* hover, inputs */
  --line: #322d4a;         /* borders */
  --text: #eae8f2;
  --muted: #9b96b4;
  --accent: #ffd166;       /* warm gold, small highlights only */
  --holo: linear-gradient(100deg, #58e6ff 0%, #b78cff 30%, #ff8bd2 60%, #ffd97a 100%);
  --radius: 12px;          /* rounded like a sleeved card */
}

* { box-sizing: border-box; }

body {
  margin: 0;
  background: var(--ink);
  color: var(--text);
  font-family: "Archivo", system-ui, sans-serif;
  line-height: 1.55;
  min-height: 100vh;
  display: flex;
  flex-direction: column;
}

main {
  flex: 1;
  width: min(960px, 100% - 2.5rem);
  margin: 0 auto;
  padding: 2rem 0 4rem;
}

a { color: var(--text); }
a:hover { color: #fff; }
:focus-visible { outline: 2px solid #b78cff; outline-offset: 2px; }

h1, h2 {
  font-family: "Unbounded", "Archivo", sans-serif;
  font-weight: 900;
  line-height: 1.1;
  letter-spacing: -0.01em;
}
h1 { font-size: clamp(1.6rem, 4vw, 2.4rem); margin: 0 0 1rem; }

.section-label {
  font-family: "Archivo", sans-serif;
  font-size: 0.8rem;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.14em;
  color: var(--muted);
  margin: 2.5rem 0 1rem;
}

/* Viewing-as banner: unmissable on purpose, above everything. */
.view-banner {
  position: sticky;
  top: 0;
  z-index: 50;
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 1rem;
  flex-wrap: wrap;
  padding: 0.55rem 1rem;
  background: #7a2a2a;
  color: #ffe9e9;
  font-size: 0.88rem;
  font-weight: 600;
}
.view-banner button {
  background: #ffe9e9;
  color: #7a2a2a;
  border: 0;
  border-radius: 999px;
  padding: 0.25rem 0.9rem;
  font: inherit;
  font-weight: 700;
  cursor: pointer;
}

/* ---- header / footer ---- */

.site-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1rem;
  padding: 1rem 1.25rem;
  border-bottom: 1px solid var(--line);
}

.wordmark {
  font-family: "Unbounded", sans-serif;
  font-weight: 900;
  font-size: 1.5rem;
  letter-spacing: 0.04em;
  text-decoration: none;
  background: var(--holo);
  background-size: 300% 100%;
  -webkit-background-clip: text;
  background-clip: text;
  color: transparent;
  animation: holo-drift 9s ease-in-out infinite alternate;
}

@keyframes holo-drift {
  from { background-position: 0% 50%; }
  to   { background-position: 100% 50%; }
}
@media (prefers-reduced-motion: reduce) {
  .wordmark, .card-hit::before { animation: none !important; }
}

.site-header nav {
  display: flex;
  align-items: center;
  gap: 1.1rem;
  font-size: 0.95rem;
}
.site-header nav a { text-decoration: none; color: var(--muted); }
.site-header nav a:hover { color: var(--text); }
.nav-cta {
  color: var(--ink) !important;
  background: var(--accent);
  padding: 0.35rem 0.9rem;
  border-radius: 999px;
  font-weight: 700;
}
.admin-link { color: var(--accent) !important; }

.site-footer {
  border-top: 1px solid var(--line);
  padding: 1.5rem 1.25rem;
  text-align: center;
  color: var(--muted);
  font-size: 0.9rem;
}
.site-footer p { margin: 0.2rem 0; }

.fineprint { color: var(--muted); font-size: 0.85rem; }
.sync-error { color: #ffb3c0; }
.promo-error { color: #ffb3c0; font-size: 0.9rem; margin: 0.35rem 0 0; }
/* The way out of the entry screen, back to the order workspace. */
.admin-return { margin: 0 0 0.6rem; }
.admin-return a { color: var(--muted); text-decoration: none; }
.admin-return a:hover { color: var(--text); text-decoration: underline; }

/* The settings hub: each area is a link plus what lives there. */
.settings-index { list-style: none; margin: 0 0 1.75rem; padding: 0; }
.settings-index li {
  display: flex; gap: 1rem; align-items: baseline;
  padding: 0.6rem 0; border-bottom: 1px solid var(--line);
}
.settings-index li a { font-weight: 700; min-width: 9.5rem; flex-shrink: 0; }
.settings-index li span { color: var(--muted); font-size: 0.9rem; }
@media (max-width: 599px) {
  .settings-index li { flex-direction: column; gap: 0.15rem; }
}
/* The rename form shares its cell with nothing — input takes the room.
   Doubled class outranks .inline-form's display, declared later. On a
   phone the pinned first column would crush the input to a sliver, so
   the form stacks and the input keeps a readable width. */
.inline-form.type-rename { display: inline-flex; gap: 0.4rem; align-items: center; }
.type-rename input { max-width: 16rem; }
@media (max-width: 899px) {
  .inline-form.type-rename { flex-direction: column; align-items: stretch; gap: 0.3rem; }
  .type-rename input { width: 10rem; max-width: none; }
}
.optional { color: var(--muted); font-weight: 400; }
.empty { color: var(--muted); }

/* ---- hero ---- */

.hero { padding: 3rem 0 1rem; }
.hero h1 { font-size: clamp(2.2rem, 6vw, 3.6rem); margin: 0; }
.holo-text {
  background: var(--holo);
  -webkit-background-clip: text;
  background-clip: text;
  color: transparent;
}
.hero-sub {
  max-width: 34rem;
  color: var(--muted);
  font-size: 1.05rem;
}

/* The hero's one job is to hand a shopper the catalog. Without this the first
   screen on a phone is all headline and video and nothing tappable. */
.hero-cta { margin-top: 0.4rem; }

.hero-with-video { display: flex; align-items: center; justify-content: space-between; gap: 2.5rem; flex-wrap: wrap; }
.hero-with-video .hero-text { flex: 1 1 22rem; }
.hero-loop {
  width: min(280px, 40vw);
  aspect-ratio: 9 / 16;
  object-fit: cover;
  border-radius: var(--radius);
  border: 1px solid var(--line);
  background: #000;
  flex-shrink: 0;
}
@media (prefers-reduced-motion: reduce) { .hero-loop { display: none; } }
/* On a phone the video wraps onto its own row, and the 40vw cap sized for the
   side-by-side desktop hero leaves it a thumbnail stranded at the left edge.
   Give it most of the column, centred.

   The height cap is a fold budget, not a taste call: header + headline + sub +
   the Shop packs button already spend ~370px, so the loop gets whatever is
   left before "Packs on the table" and the top of the first card. At 30vh
   that's 200px on a 667px phone, which lands the heading around y=640 —
   inside the first screen, where the catalog belongs. */
@media (max-width: 760px) {
  .hero { padding: 1.5rem 0 0.5rem; }
  .hero-with-video { gap: 1.1rem; }
  .hero-loop {
    width: min(320px, 76vw);
    max-height: 30vh;
    margin: 0 auto;
  }
  .hero-cta { margin-top: 0.2rem; }
  /* The hero already reads as a section break; the label doesn't need a
     second one on top of it. */
  .hero + .pack-grid-section > .section-label { margin-top: 1.5rem; }
}
.fresh-strip { margin-top: 0.5rem; }
.fresh-strip .section-label { margin: 1rem 0 0.7rem; }
.fresh-row {
  list-style: none;
  padding: 0 0 0.4rem;
  margin: 0;
  display: flex;
  gap: 0.8rem;
  overflow-x: auto;
  -webkit-overflow-scrolling: touch;
}
.fresh-tile { display: block; width: 118px; text-decoration: none; }
.fresh-thumb {
  display: block;
  width: 118px;
  aspect-ratio: 9 / 16;
  border-radius: 10px;
  overflow: hidden;
  background: var(--surface-2);
  border: 1px solid var(--line);
}
.fresh-thumb img { width: 100%; height: 100%; object-fit: cover; display: block; transition: transform 0.15s ease; }
.fresh-tile:hover .fresh-thumb img { transform: scale(1.04); }
.fresh-play { display: grid; place-items: center; width: 100%; height: 100%; color: var(--muted); font-size: 1.4rem; }
.fresh-caption {
  display: block;
  margin-top: 0.35rem;
  font-size: 0.72rem;
  color: var(--muted);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

/* ---- pack grid ---- */

.pack-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
  gap: 1.25rem;
}
/* minmax(200px) resolves to one column on a phone, so a two-pack store costs
   two full screens of scrolling. Two up instead — same breakpoint and shape
   as the fresh-rips row above. */
@media (max-width: 760px) {
  .pack-grid {
    grid-template-columns: repeat(2, minmax(0, 1fr));
    gap: 0.9rem 0.7rem;
  }
  /* Half-width cards leave the corner chip less room; keep it on one line. */
  .pack-grid .preorder-chip {
    top: 0.5rem;
    right: 0.5rem;
    font-size: 0.62rem;
    letter-spacing: 0.06em;
    padding: 0.15rem 0.45rem;
  }
  .pack-grid .pack-card-body { padding: 0.7rem 0.7rem 0.85rem; }
  /* Price and stock share one line when they fit; the long preorder line
     drops under the price instead of squeezing it. */
  .pack-grid .pack-meta { flex-wrap: wrap; gap: 0 0.5rem; }
  .pack-grid .pack-art-placeholder { font-size: 2.2rem; }
}

.pack-card {
  position: relative;
  display: flex;
  flex-direction: column;
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  overflow: hidden;
  text-decoration: none;
  transition: transform 0.15s ease, border-color 0.15s ease;
}
.pack-card:hover { transform: translateY(-3px); border-color: #4a4370; }
/* Sold out: still browsable, visually stepped back so in-stock packs lead. */
.pack-card-out { opacity: 0.5; }
.pack-card-out img { filter: saturate(0.45); }
.pack-card-out:hover { opacity: 0.85; }
.pack-card img {
  width: 100%;
  aspect-ratio: 3 / 4;
  /* contain, not cover: product shots are tall transparent pack renders —
     cover would crop them; contain floats the full pack on the card */
  object-fit: contain;
  padding: 1rem 1rem 0;
}

.pack-art-placeholder {
  display: grid;
  place-items: center;
  aspect-ratio: 3 / 4;
  background: var(--surface-2);
  color: var(--line);
  font-family: "Unbounded", sans-serif;
  font-size: 3rem;
  font-weight: 900;
}
.pack-art-placeholder.large { border-radius: var(--radius); }

/* Grid cards stretch to a shared height, so a short name leaves dead space at
   the bottom of its card and its price floats up out of line with the card
   next to it. Let the body take the slack and pin the price to the bottom, and
   every price in a row sits on one baseline. */
.pack-card-body { display: flex; flex-direction: column; flex: 1; gap: 0.2rem; padding: 0.8rem 0.9rem 1rem; }
.pack-game {
  font-size: 0.72rem;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.12em;
  color: var(--muted);
}
.pack-name { font-weight: 700; }
.pack-meta { display: flex; justify-content: space-between; align-items: baseline; margin-top: auto; padding-top: 0.3rem; }
.pack-price { font-weight: 700; color: var(--accent); }
.pack-stock { font-size: 0.8rem; color: var(--muted); }
.sold-out { color: #ff8b8b; font-weight: 700; }

/* Preorder chrome — derived from the set's release date, never a flag. */
.preorder-chip {
  position: absolute;
  top: 0.7rem;
  right: 0.7rem;
  z-index: 1;
  font-size: 0.68rem;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.08em;
  color: var(--accent);
  border: 1px solid var(--accent);
  border-radius: 999px;
  padding: 0.18rem 0.55rem;
  background: rgba(20, 18, 31, 0.85);
}
.preorder-chip.inline {
  position: static;
  margin-left: 0.4rem;
  vertical-align: middle;
}

/* ---- product page ---- */

.product-page {
  display: grid;
  grid-template-columns: minmax(200px, 320px) 1fr;
  gap: 2rem;
  align-items: start;
}
/* Stacked on a phone the 3/4 art at full width is a ~480px block, which puts
   the price and the Buy button off the first screen. Cap it so the pack name,
   the price and the CTA all land inside the first paint. */
@media (max-width: 640px) {
  .product-page { grid-template-columns: 1fr; gap: 1.2rem; }
  .product-art { width: min(58%, 230px); margin: 0 auto; }
  .product-art img,
  .product-art .pack-art-placeholder { max-height: 38vh; object-fit: contain; }
  .product-art .pack-art-placeholder { font-size: 2.4rem; }
  .product-price { margin: 0.7rem 0; }
}
.product-art img { width: 100%; border-radius: var(--radius); }
.product-price { font-size: 1.6rem; font-weight: 700; color: var(--accent); margin: 1rem 0; }

/* ---- buttons & forms ---- */

.button-primary {
  display: inline-block;
  background: var(--accent);
  color: var(--ink);
  border: none;
  border-radius: 999px;
  padding: 0.7rem 1.6rem;
  font-family: "Archivo", sans-serif;
  font-size: 1rem;
  font-weight: 700;
  text-decoration: none;
  cursor: pointer;
}
.button-primary:hover { filter: brightness(1.08); color: var(--ink); }

/* Every bare <button> gets the house secondary look — new admin buttons
   are styled by default instead of falling back to the browser's. Any
   class (chip, button-primary, button-ghost, link-button…) outranks this. */
button {
  background: var(--surface-2);
  color: var(--text);
  border: 1px solid var(--line);
  border-radius: 8px;
  padding: 0.45rem 1rem;
  font: inherit;
  font-weight: 600;
  cursor: pointer;
}
button:hover { border-color: #4a4370; color: #fff; }
button:disabled { opacity: 0.5; cursor: default; }

.inline-form { display: inline; }
.link-button {
  background: none;
  border: none;
  padding: 0;
  color: var(--muted);
  font: inherit;
  font-size: 0.95rem;
  white-space: nowrap;
  cursor: pointer;
}
.link-button:hover { color: var(--text); }

/* A bare <button> — plenty of them in the back office — wears the app's
   secondary look instead of the OS default. Element-level on purpose: every
   .chip / .link-button / .button-primary still wins on specificity. */
button {
  background: var(--surface-2);
  border: 1px solid var(--line);
  color: var(--text);
  border-radius: 8px;
  padding: 0.5rem 1rem;
  font: inherit;
  font-weight: 700;
  cursor: pointer;
}
button:hover { border-color: #4a4370; }

form label { display: block; margin: 0 0 0.9rem; font-weight: 500; font-size: 0.95rem; }
input[type="text"], input[type="email"], input[type="password"],
input[type="url"], input[type="number"], input[type="search"],
input[type="tel"], input[type="date"], input[type="datetime-local"],
textarea, select {
  display: block;
  width: 100%;
  margin-top: 0.3rem;
  padding: 0.55rem 0.7rem;
  background: var(--surface-2);
  border: 1px solid var(--line);
  border-radius: 8px;
  color: var(--text);
  font: inherit;
}
input:focus, textarea:focus, select:focus { border-color: #b78cff; outline: none; }

/* File pickers were the one control left raw — same box as the rest. */
input[type="file"] {
  display: block;
  width: 100%;
  max-width: 100%;
  margin-top: 0.3rem;
  padding: 0.45rem 0.6rem;
  background: var(--surface-2);
  border: 1px solid var(--line);
  border-radius: 8px;
  color: var(--text);
  font: inherit;
}
input[type="file"]::file-selector-button {
  margin-right: 0.7rem;
  padding: 0.35rem 0.8rem;
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: 6px;
  color: var(--text);
  font: inherit;
  font-weight: 700;
  cursor: pointer;
}
input[type="search"] { -webkit-appearance: none; appearance: none; }
input[type="search"]::-webkit-search-decoration { -webkit-appearance: none; }
input[type="checkbox"], input[type="radio"] { accent-color: var(--accent); }

.auth-card {
  max-width: 26rem;
  margin: 2rem auto 0;
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  padding: 2rem;
}
.auth-card h1 { font-size: 1.5rem; }
.auth-card .button-primary { width: 100%; margin-top: 0.5rem; }
.auth-alt { margin-top: 1.2rem; font-size: 0.9rem; color: var(--muted); }
/* Requirement said once, under the field, instead of only after a failed submit. */
.field-hint {
  display: block;
  margin-top: 0.35rem;
  color: var(--muted);
  font-weight: 400;
  font-size: 0.85rem;
}

.flash {
  border-radius: 8px;
  padding: 0.7rem 1rem;
  margin: 0 0 1.2rem;
  font-size: 0.95rem;
}
.flash-error { background: #3a1e2a; border: 1px solid #6e2b40; color: #ffb3c0; }
/* A flash that names a fix links to it — inheriting the flash's own color so
   the sentence stays one sentence. */
.flash a { color: inherit; text-decoration: underline; text-underline-offset: 3px; }
.flash a:hover { color: #fff; }
.flash-info { background: #1e2f3a; border: 1px solid #2b556e; color: #b3dcff; }

/* ---- rips ---- */

.rip-list { list-style: none; margin: 1rem 0 0; padding: 0; display: flex; flex-direction: column; gap: 0.7rem; }
.rip-row {
  display: flex;
  justify-content: space-between;
  align-items: center;
  gap: 1rem;
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  padding: 0.9rem 1.1rem;
  text-decoration: none;
}
.rip-row:hover { border-color: #4a4370; }
.rip-row-main { display: flex; flex-direction: column; gap: 0.1rem; }
.rip-title { font-weight: 700; }
/* Order date + rip number: several rips of the same pack are the normal
   case, so this line is what tells two rows apart. */
.rip-meta { font-size: 0.85rem; color: var(--muted); }
.rip-row-meta { display: flex; align-items: center; gap: 0.6rem; flex-shrink: 0; }

.status {
  font-size: 0.78rem;
  font-weight: 700;
  padding: 0.2rem 0.7rem;
  border-radius: 999px;
  white-space: nowrap;
}
/* One hue per workflow stage, so the queue reads at a glance:
   gold = rip it, blue = inventorying, purple = with the buyer,
   green = finalized (pull + ship), teal = shipped, gray = dead. */
.status-pending { background: #3a1e2a; color: #ffb3c0; }
.status-canceled { background: var(--surface-2); color: var(--muted); }
.status-queued { background: #3a2f14; color: #ffd166; }
.status-ripped { background: #1e2b3a; color: #9ac4f0; }
.status-submitted { background: #2c1e3a; color: #cbaaf5; }
.status-finalized { background: #1e3a26; color: #9af0b5; }
.status-shipped { background: #1e3a38; color: #8fe0d8; }
/* Hand of God: the manual end state — radiant gold, unlike any stage hue. */
.status-god { background: #3a3418; color: #ffe08e; }

/* Admins get a quiet way from a storefront page into its admin twin. */
.admin-edit-link { color: var(--muted); text-decoration: none; }
.admin-edit-link:hover { color: var(--text); text-decoration: underline; }

.rip-header { display: flex; flex-direction: column; align-items: flex-start; gap: 0.4rem; margin-bottom: 1.5rem; }
.rip-header h1 { margin: 0; }
.rip-note { color: var(--muted); max-width: 34rem; }

/* Reserve the frame before metadata arrives: a bare <video> renders at the
   UA default 300x150 and then jumps to its real height, shoving the pull
   list down the screen right as the page settles. Matches the iframe rule. */
.rip-video video {
  width: 100%;
  aspect-ratio: 16 / 9;
  border-radius: var(--radius);
  background: #000;
}
.rip-video iframe {
  width: 100%;
  aspect-ratio: 16 / 9; /* real ratio arrives inline once Stream reports it */
  border: none;
  border-radius: var(--radius);
  background: #000;
}

/* Vertical rips go TikTok-style: the video holds the left column (sticky,
   so the pull keeps playing while picks happen) and the cards take the
   right. Horizontal/legacy videos keep the stacked layout. */
.rip-body-vertical {
  display: grid;
  grid-template-columns: minmax(260px, 380px) 1fr;
  gap: 2rem;
  align-items: start;
}
.rip-body-vertical .rip-video-col {
  position: sticky;
  top: 1.5rem;
}
.rip-body-vertical .rip-main-col .section-label:first-child { margin-top: 0; }
@media (max-width: 860px) {
  .rip-body-vertical { display: block; }
  .rip-body-vertical .rip-video-col {
    position: static;
    max-width: 420px;
    margin: 0 auto 1.5rem;
  }
}

/* A submitted rip with no playable video yet: say so, rather than leaving a
   0-height hole where the player belongs. */
.rip-video-pending {
  margin: 0 0 1.2rem;
  padding: 0.9rem 1rem;
  max-width: 34rem;
  border: 1px dashed var(--line);
  border-radius: var(--radius);
  background: var(--surface);
  color: var(--muted);
}

.video-share { margin: 0.6rem 0 0; font-size: 0.95rem; }

/* Share and save are the two things a buyer wants the moment the rip lands.
   They're buttons, not a sentence — the Discord line reads underneath. */
.video-actions { display: flex; flex-wrap: wrap; gap: 0.6rem; margin: 0.9rem 0 0; }
.video-actions .button-ghost { padding: 0.55rem 1.2rem; font-size: 0.95rem; }
.video-share-note { margin: 0.6rem 0 0; max-width: 34rem; }

/* The honest version of a player that won't play. */
.rip-video-error {
  margin: 0.7rem 0 0;
  padding: 0.9rem 1rem;
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background: var(--surface);
  color: var(--muted);
}

.video-upload { margin-bottom: 1rem; }
.video-upload label { margin-bottom: 0.5rem; }
.upload-progress {
  height: 8px;
  border-radius: 999px;
  background: var(--surface-2);
  overflow: hidden;
  margin: 0.6rem 0 0.3rem;
}
.upload-progress-bar {
  height: 100%;
  width: 0;
  border-radius: 999px;
  background: var(--holo);
  transition: width 0.2s ease;
}

/* ---- cards in a rip ---- */

.card-list { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 0.6rem; }
.card-row {
  position: relative;
  display: flex;
  align-items: center;
  gap: 1rem;
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  padding: 0.75rem 1rem;
}
.card-main { display: flex; flex-direction: column; gap: 0.1rem; flex: 1; min-width: 0; }
.card-name { font-weight: 700; }
.card-detail { font-size: 0.82rem; color: var(--muted); }
.card-value { font-weight: 700; white-space: nowrap; display: flex; flex-direction: column; align-items: flex-end; gap: 0.1rem; }
.credit-main { font-weight: 700; }
.market-ref { font-size: 0.72rem; font-weight: 400; color: var(--muted); }
.card-choice { display: flex; gap: 0.4rem; flex-shrink: 0; }

/* A pick that failed on the way to the server. htmx swaps nothing on an
   error, so the row still shows the old chip — it has to say so itself,
   right where the thumb tapped, not in a tally a screen away. The wrap only
   turns on for the failing row, so nothing else moves. */
.card-row.card-row-error { flex-wrap: wrap; border-color: #6e2b40; }
.pick-error {
  flex: 1 1 100%;
  font-size: 0.875rem;
  font-weight: 700;
  color: #ffb3c0;
}

/* Card art, big enough to read. Same dialog shape as the rip viewer. */
.card-lightbox {
  width: 100vw; height: 100dvh; max-width: none; max-height: none;
  margin: 0; padding: 0; border: 0; background: transparent; overflow: hidden;
}
.card-lightbox::backdrop { background: rgba(10, 8, 18, 0.92); backdrop-filter: blur(18px); }
.card-lb-stage {
  width: 100%; height: 100%;
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  gap: 0.9rem; padding: 1.5rem;
}
.card-lb-stage img {
  max-width: min(92vw, 420px);
  max-height: 76vh;
  width: auto; height: auto;
  display: block;
  border-radius: 12px;
  background: #14121f;
}
.card-lb-name { margin: 0; color: #fff; font-weight: 700; text-align: center; }

.escrow-note { background: color-mix(in srgb, var(--accent) 8%, transparent); border: 1px solid color-mix(in srgb, var(--accent) 25%, transparent); border-radius: 8px; padding: 0.7rem 0.9rem; font-size: 0.9rem; }

/* What the locked picks turned into. Sits above the pull list on every
   finalized rip so the answer to "so what happens now?" is the first thing
   read, whether the credit landed at finalize or is waiting on an escrow. */
.rip-outcome {
  background: var(--surface);
  border: 1px solid var(--line);
  border-left: 3px solid var(--accent);
  border-radius: var(--radius);
  padding: 0.9rem 1.1rem 1rem;
  margin-bottom: 1.5rem;
}
.rip-outcome .section-label { margin: 0 0 0.7rem; }
.outcome-list {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: 0.7rem;
}
.outcome-list li { display: flex; flex-direction: column; gap: 0.15rem; }
.outcome-label {
  font-size: 0.72rem;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.12em;
  color: var(--muted);
}
.outcome-text { line-height: 1.45; }

/* Tracking gets its own tappable chip rather than sitting inside the
   sentence — nothing auto-links a carrier number, and selecting 22 digits
   with a thumb is the worst part of a shipped order. */
.track-row { display: flex; flex-wrap: wrap; align-items: center; gap: 0.5rem; margin-top: 0.45rem; }
.track-link {
  display: inline-flex;
  align-items: center;
  flex-wrap: wrap; /* a 22-digit label + carrier is wider than a 320px phone */
  gap: 0.15rem 0.55rem;
  padding: 0.4rem 0.85rem;
  border: 1px solid var(--line);
  border-radius: 8px;
  background: var(--surface-2);
  text-decoration: none;
}
a.track-link:hover { border-color: var(--accent); color: var(--text); }
/* Full size, not a micro-label: the carrier is half of what the buyer
   reads out loud when they go looking for the package. */
.track-carrier { font-weight: 700; color: var(--muted); }
.track-number { font-weight: 700; min-width: 0; overflow-wrap: anywhere; }

/* Product-page rips: one Cameo-style row — staff pick first, unselected
   latest after it, each tile playing in place on tap. The frame owns its
   size forever: poster and video both fill it, so the play swap can never
   move a pixel of layout. */
.product-rips { margin-top: 2.4rem; }
.product-rip-row { list-style: none; display: flex; gap: 0.9rem; padding: 0; margin: 0.7rem 0 0.4rem; flex-wrap: wrap; align-items: flex-start; }
.rip-tile { display: flex; flex-direction: column; gap: 0.35rem; width: 170px; }
.rip-tile-play {
  position: relative; display: block; width: 100%; padding: 0; cursor: pointer;
  aspect-ratio: 9 / 16; /* house vertical default; each tile overrides with its video's true ratio */
  border: 1px solid var(--line); border-radius: var(--radius); background: #000; overflow: hidden;
}
.rip-tile-play img, .rip-tile-play video, .rip-thumb-placeholder {
  position: absolute; inset: 0; width: 100%; height: 100%;
  object-fit: cover; display: block; background: #000; border: 0;
}
/* Posterless tiles were solid black rectangles, which read as a broken
   player. Same surface-2 treatment as .pack-art-placeholder, with a soft
   violet wash, so an empty frame reads as "video lives here". */
.rip-thumb-placeholder {
  background: linear-gradient(160deg, var(--surface-2), var(--surface));
  border: 0;
}
.rip-tile-play.playing { cursor: pointer; }
.rip-tile-play.playing .play-badge { display: none; }
.rip-tile-play.playing.paused .play-badge { display: flex; }
button.rip-tile-play:hover, a.rip-tile-play:hover { border-color: var(--accent); }
.play-badge {
  position: absolute; inset: 0; margin: auto; width: 46px; height: 46px;
  display: flex; align-items: center; justify-content: center; padding-left: 4px;
  background: rgba(20, 18, 31, 0.65); color: #fff; border-radius: 50%;
  font-size: 1.05rem; pointer-events: none;
}
.rip-tile-play:hover .play-badge { background: rgba(20, 18, 31, 0.85); }
.tile-chip {
  position: absolute; top: 0.55rem; left: 0.55rem; z-index: 1;
  font-size: 0.62rem; font-weight: 700; text-transform: uppercase; letter-spacing: 0.08em;
  color: var(--accent); border: 1px solid var(--accent); border-radius: 999px;
  padding: 0.15rem 0.5rem; background: rgba(20, 18, 31, 0.85);
}
.rip-tile .card-detail a { color: inherit; }
/* 40px hit area, small glyph: the whole tile under it is a tap-to-pause
   target, so a near-miss on expand pauses the rip instead. */
.tile-expand {
  position: absolute; top: 0.5rem; right: 0.5rem; z-index: 2;
  width: 40px; height: 40px; border: 0; border-radius: 50%; cursor: pointer;
  display: flex; align-items: center; justify-content: center;
  background: rgba(20, 18, 31, 0.65); color: #fff; font-size: 1rem; line-height: 1;
}
.tile-expand:hover { background: rgba(20, 18, 31, 0.9); }
/* Native fullscreen fallback (browsers that ignore controlslist):
   letterbox the full frame instead of cover-cropping the screen. */
.rip-tile-play video:fullscreen { object-fit: contain; }
.rip-tile-play video:-webkit-full-screen { object-fit: contain; }

/* On a phone the row becomes two tiles across — a fixed 170px column left
   half the screen empty and made the page twice as long. The "Full rip"
   link under each tile also gets a thumb-sized box. */
@media (max-width: 760px) {
  /* Scoped to the row, not its page wrapper — the ripper profile renders
     the same reel outside .product-rips. */
  .product-rip-row {
    display: grid;
    grid-template-columns: repeat(2, minmax(0, 1fr));
    gap: 0.9rem 0.7rem;
  }
  .product-rip-row .rip-tile { width: auto; }
  .rip-tile .card-detail a {
    display: inline-flex;
    align-items: center;
    min-height: 40px;
    padding: 0.35rem 0.4rem;
    margin: -0.35rem -0.4rem;
  }
}

/* The rip story viewer: full-screen overlay, active rip centered with
   custom controls + buy CTA, neighbors as dimmed side cards (Cameo). */
.video-lightbox { width: 100vw; height: 100dvh; max-width: none; max-height: none; margin: 0; padding: 0; border: 0; background: transparent; overflow: hidden; }
.video-lightbox::backdrop { background: rgba(10, 8, 18, 0.92); backdrop-filter: blur(18px); }
/* Fixed, not absolute: the button lives inside .lb-card so it can anchor to
   the video's corner on a phone, but on desktop it still pins to the screen
   corner exactly as before. */
.lb-x { position: fixed; top: 1rem; right: 1.4rem; z-index: 3; background: none; border: 0; color: #fff; font-size: 1.3rem; cursor: pointer; }
.lb-stage { width: 100%; height: 100%; display: flex; align-items: center; justify-content: center; gap: 1.6rem; }
.lb-card { position: relative; height: min(88vh, 940px); flex-shrink: 0; }
.lb-card video { height: 100%; width: auto; max-width: 92vw; display: block; border-radius: var(--radius); background: #000; cursor: pointer; }
.lb-back {
  position: absolute; top: 0.7rem; left: 0.7rem; z-index: 2;
  width: 40px; height: 40px; border: 0; border-radius: 50%; cursor: pointer;
  display: flex; align-items: center; justify-content: center;
  background: rgba(20, 18, 31, 0.65); color: #fff; font-size: 1.2rem; line-height: 1;
}
.lb-back:hover { background: rgba(20, 18, 31, 0.9); }
.lb-overlay {
  position: absolute; left: 0; right: 0; bottom: 0; z-index: 2;
  padding: 2rem 0.9rem 0.9rem; display: flex; flex-direction: column; gap: 0.55rem;
  background: linear-gradient(transparent, rgba(0, 0, 0, 0.7));
  border-radius: 0 0 var(--radius) var(--radius);
}
.lb-controls { display: flex; align-items: center; gap: 0.35rem; color: #fff; font-size: 0.8rem; font-variant-numeric: tabular-nums; }
/* Transport buttons were exactly as big as their emoji (10x40, 16x40), so a
   thumb aiming at pause hit the video behind it. Same glyph, 40x40 target. */
.lb-controls button {
  background: none; border: 0; color: #fff; cursor: pointer;
  font-size: 1rem; line-height: 1; padding: 0;
  flex: 0 0 auto; width: 40px; height: 40px;
  display: flex; align-items: center; justify-content: center;
}
/* A 16px-tall seek bar is not grabbable while it moves — give the track a
   thumb-sized touch height and a bigger knob, keeping the bar itself thin. */
#lb-scrub {
  flex: 1; accent-color: var(--accent); min-width: 0;
  -webkit-appearance: none; appearance: none;
  height: 36px; margin: 0; padding: 0; background: transparent; cursor: pointer;
}
#lb-scrub::-webkit-slider-runnable-track {
  height: 4px; border-radius: 999px; background: rgba(255, 255, 255, 0.32);
}
#lb-scrub::-moz-range-track {
  height: 4px; border-radius: 999px; background: rgba(255, 255, 255, 0.32);
}
#lb-scrub::-moz-range-progress {
  height: 4px; border-radius: 999px; background: var(--accent);
}
#lb-scrub::-webkit-slider-thumb {
  -webkit-appearance: none; appearance: none;
  width: 18px; height: 18px; margin-top: -7px;
  border: 0; border-radius: 50%; background: #fff;
}
#lb-scrub::-moz-range-thumb {
  width: 18px; height: 18px; border: 0; border-radius: 50%; background: #fff;
}
#lb-scrub:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.lb-cta {
  display: block; width: 100%; box-sizing: border-box; text-align: center;
  background: rgba(20, 18, 31, 0.75); border: 1px solid var(--line); color: #fff;
  border-radius: 999px; padding: 0.6rem 1rem; font: inherit; font-weight: 700;
  text-decoration: none; cursor: pointer;
}
.lb-cta:hover { border-color: var(--accent); }
.lb-cta-form { margin: 0; }
.lb-side { border: 0; padding: 0; background: #000; border-radius: var(--radius); overflow: hidden; cursor: pointer; opacity: 0.4; height: 42vh; flex-shrink: 0; }
.lb-side:hover { opacity: 0.65; }
.lb-side img { height: 100%; width: auto; display: block; }
.lb-arrow {
  width: 42px; height: 42px; flex-shrink: 0; border: 0; border-radius: 50%; cursor: pointer;
  background: rgba(255, 255, 255, 0.14); color: #fff; font-size: 1.3rem; line-height: 1;
}
.lb-arrow:hover { background: rgba(255, 255, 255, 0.28); }
/* On a phone the rip is the screen: the video goes full-bleed and the
   prev/next arrows float on top of it, instead of the old side gutter that
   squeezed a 9:16 rip into a 278px letterboxed box.

   The controls do NOT float on the video here. A rip whose dimensions we
   don't know is 16:9 (Rip.AspectRatio's default), which is only ~219px tall
   at 390px wide — an overlay pinned to the video's bottom edge covered most
   of the frame, and white timecodes on the tail of a transparent-to-black
   gradient sat on raw bright footage. So the transport row and the buy CTA
   drop *below* the frame onto their own solid bar, and the video is capped
   so the pair always fits the screen. */
@media (max-width: 900px) {
  .lb-side { display: none; }
  .lb-stage { gap: 0; position: relative; }
  .lb-card {
    height: auto; width: 100%; max-height: 100dvh;
    display: flex; flex-direction: column;
  }
  .lb-card video {
    width: 100%; max-width: none; height: auto;
    /* Room for the chrome bar underneath, so a tall rip can't push it off. */
    max-height: calc(100dvh - 10rem);
    border-radius: 0; margin: 0 auto;
  }
  .lb-overlay {
    position: static;
    padding: 0.7rem 0.9rem 0.9rem;
    background: rgba(10, 8, 18, 0.94);
    border-top: 1px solid var(--line);
    border-radius: 0;
  }
  /* Quiet chrome still has to be findable once it's off the footage. */
  .lb-overlay .lb-cta { background: var(--surface-2); }
  /* Two round "‹" buttons on the same left edge read as the same control —
     one ejects you from the viewer, one steps back a rip. The ✕ already
     closes, so the back button goes. */
  .lb-back { display: none; }
  .lb-arrow {
    /* Centered on the video, which now sits above the chrome bar. */
    position: absolute; top: calc(50% - 3.75rem); transform: translateY(-50%); z-index: 2;
    background: rgba(20, 18, 31, 0.65);
  }
  .lb-arrow:hover { background: rgba(20, 18, 31, 0.9); }
  #lb-prev { left: 0.6rem; }
  #lb-next { right: 0.6rem; }
  /* Close sits on the video's corner, not the screen's — on a short 16:9 rip
     the screen corner is hundreds of px above the frame it closes. And it's
     on top of the video, so a near-miss would toggle play. */
  .lb-x {
    position: absolute;
    top: 0.45rem; right: 0.45rem;
    width: 44px; height: 44px; display: flex; align-items: center; justify-content: center;
    /* On mobile this is the only way out — .lb-back is hidden — and rip
       footage is often bright, so a bare white glyph can vanish. Same
       scrim the other floating controls wear. */
    background: rgba(20, 18, 31, 0.65); border-radius: 50%;
  }
}

/* Public rip page: media + provenance, storefront-framed. */
.public-rip-head h1 { margin-bottom: 0.3rem; }
.public-rip-body { display: flex; gap: 2rem; align-items: flex-start; flex-wrap: wrap; margin-top: 1.2rem; }
.public-video { flex: 0 1 300px; }
.public-video iframe, .public-video video { width: 100%; border: 0; border-radius: var(--radius); background: #000; }
.player-frame { position: relative; display: block; line-height: 0; }
/* The rip starts playing on arrival, and browsers only allow that muted, so
   sound is the first thing worth reaching for. Top-left: the bottom strip is
   the player's own controls and the top-right corner carries the watermark.
   Bare glyph, no chip — it sits over the footage the way a feed player's
   does. Rip footage is often bright, so the icon carries its own shadow
   rather than a panel. Resets the base <button> styling wholesale. */
.sound-toggle {
  position: absolute; top: 0.35rem; left: 0.35rem;
  display: flex; align-items: center; justify-content: center;
  width: 2.75rem; height: 2.75rem; padding: 0;
  color: #fff; background: none; border: 0; border-radius: 8px;
  cursor: pointer;
  /* Two shadows, not one: the tight one gives the glyph an edge, the soft
     halo keeps it legible when the rip is shot against a white wall. */
  filter: drop-shadow(0 1px 2px rgba(0, 0, 0, 0.7))
          drop-shadow(0 0 7px rgba(0, 0, 0, 0.5));
}
/* display: flex above outranks the UA [hidden] rule; restate it. */
.sound-toggle[hidden] { display: none; }
.sound-toggle:hover { background: none; opacity: 0.85; }
.sound-toggle svg { display: block; width: 1.6rem; height: 1.6rem; }
.sound-toggle:focus-visible { outline: 2px solid #fff; outline-offset: -6px; }
.public-rip-body:not(.vertical) .public-video { flex-basis: 480px; }
/* No video on the file yet: hold the player's place and say so, rather than
   leaving a share button floating in empty space. */
.video-pending {
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  gap: 0.5rem; width: 100%; aspect-ratio: 16 / 9; min-height: 9rem;
  padding: 1rem; text-align: center;
  background: var(--surface); border: 1px dashed var(--line); border-radius: var(--radius);
}
.video-pending p { margin: 0; color: var(--muted); font-size: 0.95rem; max-width: 24rem; }
.video-pending-mark { font-size: 1.6rem; color: var(--muted); opacity: 0.5; }
.public-rip-main { flex: 1 1 24rem; min-width: 0; }
.public-explainer { background: var(--surface); border: 1px solid var(--line); border-radius: var(--radius); padding: 1rem 1.2rem; margin-bottom: 0.6rem; }
.public-explainer .button-primary { margin-top: 0.4rem; }

.share-dialog { border: 1px solid var(--line); border-radius: var(--radius); background: var(--surface); color: var(--text); padding: 1.4rem 1.6rem; max-width: 26rem; width: calc(100% - 2rem); }
.share-dialog::backdrop { background: rgba(10, 8, 18, 0.7); }
.share-dialog h2 { margin: 0 0 1rem; font-size: 1.2rem; }
.share-close { position: absolute; top: 0.8rem; right: 1rem; background: none; border: 0; color: var(--muted); font-size: 1.1rem; cursor: pointer; }
.share-grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: 0.9rem 0.4rem; margin-bottom: 1.2rem; }
.share-grid a, .share-grid button { display: flex; flex-direction: column; align-items: center; gap: 0.35rem; background: none; border: 0; color: var(--text); font: inherit; font-size: 0.78rem; text-decoration: none; cursor: pointer; }
.share-grid .share-ico { display: grid; place-items: center; width: 44px; height: 44px; border-radius: 12px; background: var(--surface-2); border: 1px solid var(--line); font-size: 1.2rem; }
.share-grid a:hover .share-ico, .share-grid button:hover .share-ico { border-color: #b78cff; }
.share-link-row { display: flex; gap: 0.5rem; }
.share-link-row input { flex: 1; margin-top: 0; }
.share-link-row .button-primary { padding: 0.55rem 1.1rem; }

/* Record page: the rip phone's capture surface. */
.record-stage { position: relative; }
.record-stage video { width: 100%; max-width: 340px; aspect-ratio: 9 / 16; object-fit: cover; background: #000; border-radius: var(--radius); display: block; }
/* When there's no camera, the black box says so — in the box, not below it. */
.record-alert {
  position: absolute;
  top: 0;
  bottom: 0;
  left: 0;
  width: 100%;
  max-width: 340px;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 0.7rem;
  padding: 1.2rem;
  text-align: center;
  border-radius: var(--radius);
  background: rgba(10, 8, 18, 0.82);
}
.record-alert[hidden] { display: none; }
.record-alert-title { margin: 0; font-size: 1.05rem; font-weight: 700; color: var(--accent); }
.record-alert-body { margin: 0; font-size: 0.95rem; color: var(--text); max-width: 20rem; }
.record-alert-detail { margin: 0; font-size: 0.8rem; color: var(--muted); max-width: 20rem; word-break: break-word; }
.record-controls { display: flex; align-items: center; gap: 1rem; margin: 1rem 0 0.4rem; }
.record-btn { font-size: 1.15rem; padding: 0.9rem 2.2rem; }
.record-btn.recording { background: #ff5c5c; color: #fff; }
/* A dead button should look dead. */
.record-btn:disabled, .record-alert button:disabled { opacity: 0.42; filter: saturate(0.5); cursor: not-allowed; }
.record-timer { font-variant-numeric: tabular-nums; font-weight: 700; font-size: 1.1rem; }
/* Before the first take the timer has no digits, but the row's gap still
   reserves 1rem beside it — a notch on the right edge of the shutter that
   knocks it out of line with the viewfinder above. It comes back with the
   clock. */
.record-timer:empty { display: none; }

/* Rip console + shipments: dense, functional, power-user tables. */
.flow-icons { display: inline-flex; gap: 0.3rem; font-size: 0.95rem; white-space: nowrap; }
.flow-icons .off { filter: grayscale(1) opacity(0.22); }
/* The scissors are live — mark ripped (dim) / undo (lit) from the queue.
   display: contents keeps the button in the icon flex row; the padding +
   negative margin grows the tap target to ~40px without moving anything.
   Overlap only covers inert neighbor icons, so nothing else gets eaten. */
.flow-toggle { display: contents; }
.flow-toggle button {
  background: none; border: 0; font: inherit; font-size: 0.95rem;
  line-height: 1; cursor: pointer; padding: 0.8rem; margin: -0.8rem;
}
.flow-toggle button:hover { filter: none; transform: scale(1.2); }
.flow-legend { margin-top: 0.6rem; }

.filter-row { display: flex; gap: 0.6rem; align-items: center; margin-bottom: 1rem; }
.filter-row select { width: auto; margin-top: 0; padding: 0.4rem 0.6rem; }

.queue-table th a { color: var(--text); text-decoration: none; }
.queue-table th a:hover { color: var(--accent); }
.queue-table td, .queue-table th { padding: 0.45rem 0.6rem; }
/* The order cell stays one line — "#40 Autumn Gazelle" — keeping rows
   compact on desktop; phones let it wrap so the pinned order column
   stays narrow while the table pans under it. */
.rip-queue td:first-child { white-space: nowrap; }
@media (max-width: 760px) { .rip-queue td:first-child { white-space: normal; } }
.queue-table .num { text-align: right; }
.order-timeline { list-style: none; padding: 0; margin: 0.4rem 0 1rem; }
.order-timeline li { display: flex; gap: 0.6rem; align-items: baseline; padding: 0.25rem 0; }
.stage-mark { width: 1.2rem; text-align: center; }
.stage-done .stage-mark { color: #9af0b5; }
.stage-todo { color: var(--muted); }
.stage-name { font-weight: 600; min-width: 11rem; }

.order-video { max-width: 560px; }
.order-video.vertical { max-width: 280px; }
.order-video iframe, .order-video video, .order-video img { width: 100%; border: 0; border-radius: 10px; background: #000; display: block; }
/* Reserve the frame before metadata arrives — same reason as .rip-video: a
   bare <video> (and the encoding poster) renders at the UA default 300x150,
   then jumps to its real height and shoves the links, the record chip and
   Remove down the screen right as a thumb is reaching for them. The Stream
   iframe overrides this inline once it reports the real ratio. */
.order-video video, .order-video img { aspect-ratio: 16 / 9; object-fit: cover; }
.order-video.vertical video, .order-video.vertical img { aspect-ratio: 9 / 16; }
.breadcrumbs { font-size: 0.85rem; color: var(--muted); margin-bottom: 0.5rem; }
.breadcrumbs a { color: var(--muted); text-decoration: none; }
.breadcrumbs a:hover { color: var(--text); text-decoration: underline; }
.crumb-sep { margin: 0 0.45rem; color: var(--line); }
.crumb-here { color: var(--text); }

.order-head { display: flex; gap: 1.6rem; align-items: center; }
.order-pack {
  width: 150px;
  aspect-ratio: 3 / 4;
  object-fit: contain;
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  padding: 0.6rem;
  flex-shrink: 0;
}
@media (max-width: 640px) { .order-pack { width: 96px; } }

.order-tabs { margin: 1.4rem 0 1.6rem; }
.order-tabs .tab-dot { margin-right: 0.4rem; font-size: 0.8em; }
.order-tabs .tab-done .tab-dot, .order-tabs a.tab-done .tab-dot { color: #9af0b5; }
.order-tabs a.tab-action .tab-dot { color: var(--accent); }
.order-tabs a.tab-todo .tab-dot { color: var(--line); }
.order-shipping { padding: 0; }

.shipment { border: 1px solid var(--surface-2); border-radius: 10px; margin: 1rem 0; overflow: hidden; }
.shipment-head { display: flex; justify-content: space-between; gap: 0.8rem; padding: 0.6rem 0.9rem; background: var(--surface-2); font-weight: 600; flex-wrap: wrap; }
.shipment-body { display: flex; gap: 1rem; padding: 0.8rem 0.9rem; flex-wrap: wrap; }
.shipment-body .pull-table { flex: 1 1 24rem; margin: 0; }
.pull-table td, .pull-table th { padding: 0.35rem 0.5rem; }
.pull-thumb { width: 34px; aspect-ratio: 146 / 204; object-fit: cover; border-radius: 3px; display: block; }
.shipment-side { display: flex; flex-direction: column; gap: 0.6rem; flex: 0 1 16rem; }
.ship-form { display: flex; flex-direction: column; gap: 0.4rem; }
.fulfillment-method { display: flex; align-items: center; gap: 0.4rem; margin-bottom: 1rem; }
.hand-of-god { margin-top: 1.5rem; }
.settings-form { max-width: 26rem; display: flex; flex-direction: column; gap: 0.9rem; align-items: stretch; }
.settings-form label { display: flex; flex-direction: column; gap: 0.3rem; font-weight: 600; margin: 0; }
/* Only knobs are narrow; text fields take the column. Buttons size to
   their label instead of stretching the full form width. */
.settings-form input[type="number"] { max-width: 8rem; }
.settings-form button, .settings-form .button-primary, .settings-form .button-ghost {
  align-self: flex-start;
  width: auto;
  margin-top: 0.2rem;
}
/* A two-letter state next to a five-digit ZIP: one line, not two full-width boxes. */
.settings-form .settings-row { display: grid; grid-template-columns: 6rem 1fr; gap: 0.6rem; }

/* The signature, spent here: hits wear the foil. */
.card-hit { border: 1px solid transparent; }
.card-hit::before {
  content: "";
  position: absolute;
  inset: -1px;
  border-radius: var(--radius);
  padding: 1px;
  background: var(--holo);
  background-size: 300% 100%;
  -webkit-mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
  -webkit-mask-composite: xor;
  mask-composite: exclude;
  animation: holo-drift 7s ease-in-out infinite alternate;
  pointer-events: none;
}

.hit-badge {
  align-self: flex-start;
  font-size: 0.68rem;
  font-weight: 900;
  letter-spacing: 0.1em;
  padding: 0.1rem 0.5rem;
  border-radius: 4px;
  background: var(--holo);
  color: var(--ink);
}

/* The thumb is a button now — a bare frame around the art, sized by the img
   inside it, so nothing about the row's layout changes. */
.card-thumb-zoom {
  padding: 0;
  border: 0;
  background: none;
  line-height: 0;
  flex-shrink: 0;
  border-radius: 4px;
  cursor: zoom-in;
}
.card-thumb-zoom:focus-visible { outline: 2px solid #b78cff; outline-offset: 2px; }

.card-thumb {
  width: 42px;
  /* Reserve the image's box before it decodes: the htmx row swap re-creates
     the img element, and without a fixed ratio the row collapses for a
     frame on every Ship/Credit click. Scryfall small thumbs are 146x204. */
  aspect-ratio: 146 / 204;
  object-fit: cover;
  border-radius: 4px;
  flex-shrink: 0;
}

.foil-chip {
  font-size: 0.68rem;
  font-weight: 900;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  padding: 0.12rem 0.45rem;
  border-radius: 4px;
  background: var(--holo);
  color: #14121f;
  border: 0;
  vertical-align: middle;
}

.chip {
  border: 1px solid var(--line);
  background: var(--surface-2);
  color: var(--muted);
  border-radius: 999px;
  padding: 0.25rem 0.8rem;
  font: inherit;
  font-size: 0.82rem;
  font-weight: 700;
  cursor: pointer;
  /* A chip is a control, whether it's a <button> or an <a> — no link underline
     running through the label. */
  text-decoration: none;
}
.chip:hover { color: var(--text); border-color: #4a4370; }
/* Touch has no hover, and on cellular the swap can take most of a second —
   the press itself has to answer, or the chip reads as dead and gets tapped
   twice. */
button.chip:active, a.chip:active { transform: scale(0.96); }
button.chip:not(.chip-active):active,
a.chip:not(.chip-active):active { background: var(--line); color: var(--text); }
.inline-form.htmx-request .chip, .chip.htmx-request { opacity: 0.55; }
.chip-active { background: var(--accent); border-color: var(--accent); color: var(--ink); }
.chip-locked { cursor: default; text-transform: capitalize; }
/* Locked dispositions read at a glance: blue = it mails, green = it becomes credit. */
.chip-ship { cursor: default; text-transform: capitalize; background: #1e2b3a; color: #9ac4f0; border-color: #2c4258; }
.chip-credit { cursor: default; text-transform: capitalize; background: #1e3a26; color: #9af0b5; border-color: #2c5838; }

.finalize { margin-top: 2rem; }

/* The picks tally sits between the list and the button that spends it — on a
   phone the rows it counts are already scrolled away. */
.picks-summary {
  display: flex;
  flex-direction: column;
  gap: 0.2rem;
  margin: 0 0 0.9rem;
  padding: 0.7rem 0.9rem;
  border: 1px solid var(--line);
  border-radius: 8px;
  background: var(--surface);
  font-size: 0.95rem;
}
.picks-summary[hidden] { display: none; }
.picks-line { font-weight: 700; }
/* Undecided cards turn into credit on lock-in — the one thing worth a highlight. */
.picks-open { color: var(--accent); font-size: 0.875rem; font-weight: 500; }
.picks-line:empty, .picks-open:empty { display: none; }

/* ---- shipping ---- */

.address-form { max-width: 26rem; }
.address-row { display: flex; gap: 0.6rem; }
.address-row label { flex: 1; }
/* The address block carries its own label — without one it's just a bordered
   box that looks like the empty inputs beneath it. */
.ship-to-block { display: flex; flex-direction: column; gap: 0.25rem; }
.ship-to-block .ship-to { margin: 0; }
.ship-to {
  font-family: inherit;
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: 8px;
  padding: 0.7rem 1rem;
  margin: 0.5rem 0 1rem;
  white-space: pre-line;
}

/* ---- account ---- */

.balance { font-size: 1.1rem; }
.balance-amount { font-family: "Unbounded", sans-serif; font-weight: 700; color: var(--accent); }
.ledger { list-style: none; margin: 0; padding: 0; }
.ledger-row {
  display: flex;
  justify-content: space-between;
  gap: 1rem;
  padding: 0.6rem 0;
  border-bottom: 1px solid var(--line);
  font-size: 0.95rem;
}
.ledger-delta { font-weight: 700; }

/* ---- gift messages ---- */

.buy-row { display: flex; gap: 0.7rem; align-items: center; flex-wrap: wrap; }

.button-ghost {
  display: inline-block;
  background: transparent;
  color: var(--text);
  border: 1px solid var(--line);
  border-radius: 999px;
  padding: 0.7rem 1.4rem;
  font-size: 1rem;
  font-weight: 700;
  text-decoration: none;
}
.button-ghost:hover { border-color: var(--accent); color: var(--accent); }

.gift-page { max-width: 30rem; margin: 0 auto; }
.gift-product { display: flex; gap: 1.2rem; align-items: center; margin-bottom: 1rem; }
.gift-product img { width: 84px; }
.gift-product h1 { font-size: 1.3rem; margin: 0.1rem 0; }
.gift-product .product-price { margin: 0.2rem 0 0; font-size: 1.1rem; }

.gift-form .section-label { margin-top: 1.2rem; }
.gift-for { display: flex; gap: 0.5rem; margin-bottom: 1rem; }
.gift-for input[type="radio"] { position: absolute; opacity: 0; pointer-events: none; }
.chip-radio {
  border: 1px solid var(--line);
  background: var(--surface-2);
  color: var(--muted);
  border-radius: 999px;
  padding: 0.4rem 1.1rem;
  font-weight: 700;
  font-size: 0.92rem;
  cursor: pointer;
}
.gift-for input:checked + .chip-radio {
  background: var(--accent);
  border-color: var(--accent);
  color: var(--ink);
}
.gift-for input:focus-visible + .chip-radio { outline: 2px solid #b78cff; outline-offset: 2px; }
/* "Myself" hides the recipient-name field; the server clears it too. */
.gift-form:has(#gift-self:checked) .gift-to-wrap { display: none; }

.gift-note {
  background: var(--surface);
  border: 1px solid var(--line);
  border-left: 3px solid var(--accent);
  border-radius: 8px;
  padding: 0.7rem 1rem;
  margin-bottom: 1.2rem;
  font-size: 0.95rem;
}

.gift-callout {
  background: var(--surface);
  border: 1px solid var(--accent);
  border-radius: var(--radius);
  padding: 0.9rem 1.1rem;
  margin-bottom: 1.2rem;
  font-size: 1.05rem;
}
.gift-callout-label {
  display: block;
  font-size: 0.72rem;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.12em;
  color: var(--accent);
  margin-bottom: 0.3rem;
}

/* ---- guides (rendered markdown) ---- */

.guide-content { max-width: 44rem; }
.guide-content h1 { font-size: 1.7rem; margin: 0 0 1rem; }
.guide-content h2 { font-size: 1.15rem; margin: 2rem 0 0.6rem; }
.guide-content h3 { font-size: 1rem; margin: 1.4rem 0 0.4rem; }
.guide-content p, .guide-content li { line-height: 1.65; color: var(--text); }
.guide-content em { color: var(--muted); }
.guide-content hr { border: none; border-top: 1px solid var(--line); margin: 2rem 0; }
.guide-content ul, .guide-content ol { padding-left: 1.4rem; }
.guide-content li { margin: 0.35rem 0; }
.guide-content code {
  background: var(--surface-2);
  border-radius: 4px;
  padding: 0.1rem 0.35rem;
  font-size: 0.9em;
  /* Long inline tokens (env vars, API paths) break rather than push the
     page sideways. No effect inside <pre>, which stays white-space: pre. */
  overflow-wrap: break-word;
}
/* Code blocks scroll inside themselves — a long line used to drag the whole
   page sideways. rem, not em: inside <pre> the UA's monospace font-size
   re-bases em on 13px and the text comes out tiny. */
.guide-content pre {
  overflow-x: auto;
  background: var(--surface-2);
  border-radius: 8px;
  padding: 0.8rem 1rem;
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: 0.9rem;
  line-height: 1.5;
}
.guide-content pre code { background: none; padding: 0; font-size: inherit; }
.guide-content blockquote {
  border-left: 3px solid var(--line);
  margin: 1rem 0;
  padding-left: 1rem;
  color: var(--muted);
}

/* ---- admin section ---- */

/* ---- admin portal: sidebar shell ---- */

/* The back office reads as its own room: a shade warmer/lighter than the
   storefront's night-stream ink, and full-width for dense tables. */
body.admin { background: #1a1826; }
body.admin .site-header { background: #14121f; }

.admin-shell {
  flex: 1;
  width: min(1720px, 100% - 3rem);
  margin: 0 auto;
  padding: 1.5rem 0 4rem;
  display: grid;
  grid-template-columns: 180px minmax(0, 1fr);
  column-gap: 2.5rem;
  align-items: start;
}
.admin-shell > .flash { grid-column: 2; margin-top: 0; }
.admin-shell > section, .admin-shell > article { grid-column: 2; min-width: 0; }

.admin-nav {
  grid-column: 1;
  grid-row: 1 / span 99;
  display: flex;
  flex-direction: column;
  gap: 0.15rem;
  position: sticky;
  top: 1rem;
  padding-top: 0.4rem;
}
.admin-nav a {
  text-decoration: none;
  color: var(--muted);
  font-size: 0.92rem;
  font-weight: 700;
  padding: 0.35rem 0.7rem;
  border-radius: 8px;
}
.admin-nav a:hover { color: var(--text); }
.admin-nav a.active { background: var(--surface-2); color: var(--text); }
.nav-group {
  font-size: 0.68rem;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.14em;
  color: var(--line);
  margin: 1.1rem 0 0.25rem 0.7rem;
}
.admin-nav .nav-group:first-child { margin-top: 0; }
.portal-label {
  font-size: 0.72rem;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.14em;
  color: var(--accent);
  margin-right: auto;
}

/* Sub-tabs: underline style, never pills. */
.catalog-tabs { display: flex; gap: 1.4rem; margin: -0.4rem 0 1.6rem; border-bottom: 1px solid var(--line); }
.catalog-tabs a {
  text-decoration: none;
  color: var(--muted);
  font-size: 0.9rem;
  font-weight: 700;
  padding: 0.3rem 0.1rem 0.5rem;
  border-bottom: 2px solid transparent;
  margin-bottom: -1px;
}
.catalog-tabs a:hover { color: var(--text); }
.catalog-tabs a.active { color: var(--text); border-bottom-color: var(--accent); }

@media (max-width: 760px) {
  .admin-shell {
    display: block;
    width: min(1720px, 100% - 2rem);
    padding-top: 1rem;
  }
  .admin-nav {
    position: static;
    flex-direction: row;
    flex-wrap: wrap;
    align-items: center;
    gap: 0 1.2rem;
    border-bottom: 1px solid var(--line);
    padding: 0;
    margin-bottom: 1.3rem;
  }
  /* Group headings are sidebar furniture. In a wrapping row they land
     mid-line and end up labelling the wrong links, so they sit this out. */
  .admin-nav .nav-group { display: none; }
  .admin-nav a {
    display: inline-flex;
    align-items: center;
    min-height: 40px;
    padding: 0 0.1rem;
    border-radius: 0;
    border-bottom: 2px solid transparent;
    margin-bottom: -1px;
    white-space: nowrap;
  }
  /* Underline, never a pill. */
  .admin-nav a.active {
    background: none;
    color: var(--text);
    border-bottom-color: var(--accent);
  }
}

.page-head { display: flex; justify-content: space-between; align-items: center; gap: 1rem; flex-wrap: wrap; }
.page-head h1 { margin: 0; }

.stat-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(160px, 1fr));
  gap: 1rem;
}
.stat {
  display: flex;
  flex-direction: column;
  gap: 0.2rem;
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  padding: 1rem 1.2rem;
  text-decoration: none;
}
a.stat:hover { border-color: #4a4370; }
.stat-value { font-family: "Unbounded", sans-serif; font-weight: 700; font-size: 1.5rem; }
.stat-label { font-size: 0.82rem; color: var(--muted); }
.stat-warn .stat-value { color: #ff8b8b; }

.admin-table {
  width: 100%;
  border-collapse: collapse;
  margin-top: 1.2rem;
  font-size: 0.95rem;
}
.admin-table th {
  text-align: left;
  font-size: 0.75rem;
  text-transform: uppercase;
  letter-spacing: 0.1em;
  color: var(--muted);
  padding: 0.5rem 0.8rem;
  border-bottom: 1px solid var(--line);
}
.admin-table td { padding: 0.65rem 0.8rem; border-bottom: 1px solid var(--line); }
.admin-table code { color: var(--muted); }
.row-inactive td { color: var(--muted); }
.cell-warn { color: #ff8b8b; font-weight: 700; }

/* ---- admin ---- */

.admin-panel {
  margin-top: 3rem;
  border: 1px dashed var(--line);
  border-radius: var(--radius);
  padding: 1.25rem;
}
/* Panel buttons inherit the base <button> look; chips keep their own, which
   the old `.admin-panel button` rule used to override (danger chips came out
   grey and indistinguishable from Save). */
.card-entry { display: flex; flex-wrap: wrap; gap: 0.5rem; align-items: center; }
.card-entry input[type="text"] { width: auto; flex: 1 1 8rem; margin-top: 0; }
.card-entry .checkbox { display: flex; align-items: center; gap: 0.35rem; margin: 0; white-space: nowrap; }

.set-thumb {
  width: 28px;
  height: 28px;
  object-fit: cover;
  border-radius: 4px;
  vertical-align: middle;
  margin-right: 0.3rem;
}

.product-form-preview {
  max-width: 160px;
  border-radius: 8px;
  margin-bottom: 0.9rem;
}

/* ---- staff card search ---- */

.card-search { margin-bottom: 1.5rem; }
.search-scope { display: flex; gap: 0.4rem; margin-top: 0.5rem; }
.search-results {
  list-style: none;
  margin: 0.5rem 0 0;
  padding: 0;
  border: 1px solid var(--line);
  border-radius: var(--radius);
  max-height: 24rem;
  overflow-y: auto;
  background: var(--surface);
}
.search-results li {
  display: flex;
  align-items: center;
  gap: 0.8rem;
  padding: 0.5rem 0.8rem;
  border-bottom: 1px solid var(--line);
}
.search-results li:last-child { border-bottom: none; }
.search-results img { width: 36px; border-radius: 3px; flex-shrink: 0; }
.search-main { display: flex; flex-direction: column; gap: 0.05rem; flex: 1; min-width: 0; }
.search-name { font-weight: 700; font-size: 0.95rem; }
.search-actions { display: flex; gap: 0.4rem; flex-shrink: 0; flex-wrap: wrap; justify-content: flex-end; }
.search-empty { color: var(--muted); font-size: 0.9rem; }

.admin-card-list { list-style: none; margin: 0 0 1.5rem; padding: 0; }
.admin-card-list li {
  display: flex;
  justify-content: space-between;
  align-items: center;
  gap: 0.8rem;
  padding: 0.4rem 0;
  border-bottom: 1px solid var(--line);
  font-size: 0.92rem;
}

.manual-entry { margin: 0 0 1.5rem; }
.manual-entry summary { cursor: pointer; color: var(--muted); font-size: 0.9rem; margin-bottom: 0.7rem; }
.chip-danger:hover { border-color: #6e2b40; color: #ffb3c0; }

.video-form { display: flex; gap: 0.5rem; align-items: flex-end; flex-wrap: wrap; margin-bottom: 1.5rem; }
.video-form label { flex: 1 1 16rem; margin: 0; }
.submit-form { margin-bottom: 0.3rem; }

/* ================================================================
   Phones. Everything below is shared-shell: the header, the admin
   chrome, form fields, tap targets and the dense tables. Desktop
   (>=900px) never sees any of it.
   ================================================================ */

/* Nothing wider than the screen — media included. */
img, video, iframe { max-width: 100%; }

/* Any table that needs more room than the phone has scrolls inside its own
   box. Pages can opt in explicitly by wrapping a table in this. */
.table-scroll {
  overflow-x: auto;
  -webkit-overflow-scrolling: touch;
  max-width: 100%;
}

@media (max-width: 899px) {
  /* iOS zooms the page — and never zooms back — when you tap a field under
     16px. Every field in the app, one rule. */
  input:not([type="checkbox"]):not([type="radio"]):not([type="hidden"]):not([type="range"]),
  textarea,
  select {
    font-size: 16px;
  }
  input[type="checkbox"], input[type="radio"] { width: 20px; height: 20px; }
}

@media (max-width: 760px) {
  main { padding: 1.25rem 0 3rem; }

  /* ---- header: the wordmark and nav keep their words whole ---- */
  .site-header {
    flex-wrap: wrap;
    gap: 0.15rem 0.9rem;
    padding: 0.6rem 1rem;
  }
  /* The wordmark is the way home; make the anchor itself fill the header
     band rather than relying on the whitespace around it. */
  .wordmark {
    font-size: 1.3rem;
    white-space: nowrap;
    display: inline-flex;
    align-items: center;
    min-height: 40px;
    line-height: 1.2;
    padding: 0.5rem 0;
  }
  .site-header nav {
    flex-wrap: wrap;
    justify-content: flex-end;
    gap: 0 1rem;
    margin-left: auto;
  }
  .site-header nav a,
  .site-header nav .link-button {
    display: inline-flex;
    align-items: center;
    min-height: 40px;
    padding: 0;
    white-space: nowrap;
  }
  .site-header .nav-cta { padding: 0.45rem 0.9rem; }
  .portal-label { margin-right: 0; }

  /* ---- tap targets: thumbs, not cursors ---- */
  button:not([class]), .button-primary, .button-ghost,
  button.chip, a.chip, .chip-radio,
  .search-results button, .search-results a {
    min-height: 40px;
  }
  button.chip, a.chip, .chip-radio {
    display: inline-flex;
    align-items: center;
    padding: 0.35rem 0.9rem;
  }
  .link-button {
    display: inline-flex;
    align-items: center;
    min-height: 40px;
  }
  /* The card thumb opens the art — a thumb-sized target even at the narrow
     42px art width the wider phones still use. */
  .card-thumb-zoom {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    min-width: 44px;
    min-height: 44px;
  }
  /* The footer's YouTube and Discord links are the only route to the
     channels — thumb-sized, still inline in the sentence. */
  .site-footer .fineprint a {
    display: inline-block;
    padding: 0.6rem 0.35rem;
    margin: -0.3rem -0.35rem;
  }
  /* Prose links stay inline; these are navigation. */
  .auth-alt a, .breadcrumbs a, .video-share a, .manual-entry summary {
    display: inline-block;
    padding: 0.45rem 0;
  }
  /* Auth links are the only way out of these pages — thumb-sized. */
  .auth-alt a { padding: 0.65rem 0; }
  /* Login has two of them; stacked, they stop sitting a few px apart. */
  .auth-alt-stack .auth-alt-item { display: block; }
  .auth-alt-stack .auth-alt-sep { display: none; }
  .auth-alt-stack .auth-alt-item + .auth-alt-item { margin-top: 0.35rem; }
  .breadcrumbs { font-size: 0.875rem; }
  .filter-row { flex-wrap: wrap; }
  .filter-row select { flex: 1 1 8rem; min-width: 0; padding: 0.5rem 0.6rem; }

  /* Tab strips scroll rather than wrap into a second row of stubs. */
  .catalog-tabs {
    flex-wrap: nowrap;
    overflow-x: auto;
    scrollbar-width: none;
    gap: 1.2rem;
  }
  .catalog-tabs::-webkit-scrollbar { display: none; }
  .catalog-tabs a { padding: 0.6rem 0.1rem 0.7rem; white-space: nowrap; }

  /* Date and file pickers are native controls with fiddly innards — the
     mm/dd/yyyy segments and the Choose File button. They get the extra few
     pixels of height that a thumb needs; plain text fields don't. */
  input[type="date"],
  input[type="datetime-local"],
  input[type="file"] { min-height: 44px; }

  /* ---- dense admin tables: scroll the table, not the page ---- */
  .admin-table {
    display: block;
    overflow-x: auto;
    -webkit-overflow-scrolling: touch;
    white-space: nowrap;
  }
  .admin-table th { font-size: 0.8rem; }
  .admin-table td, .admin-table th { padding: 0.55rem 0.7rem; }
  /* Row actions and sort headers are the only way into a record. */
  .admin-table td a:not(.button-primary),
  .admin-table th a {
    display: inline-block;
    padding: 0.5rem 0.4rem;
    margin: -0.35rem -0.4rem;
  }

  /* The catalog is the widest table in the admin — eight columns, with the
     product name at one end and its Edit link at the other. Panning right
     for the price or the Edit used to leave a row of anonymous numbers, so
     the name column rides along, and its edge doubles as the cue that
     there is more table off to the right. */
  .catalog-table { border-collapse: separate; border-spacing: 0; }
  .catalog-table th:first-child,
  .catalog-table td:first-child {
    position: sticky;
    left: 0;
    z-index: 1;
    background: var(--ink);
    box-shadow: 1px 0 0 var(--line);
  }
  /* Bounded and allowed to wrap, so a long pack name can't eat the screen. */
  .catalog-table th:first-child { width: 9.5rem; }
  .catalog-table td:first-child { white-space: normal; max-width: 11rem; }

  .stat-grid { grid-template-columns: repeat(auto-fill, minmax(140px, 1fr)); gap: 0.7rem; }

  /* Some ops tiles open a list and some are just a number, and the only cue
     that told them apart was an a.stat:hover border — which never fires on a
     touch screen. Tiles that go somewhere carry a standing arrow and a lit
     border, so tapping one is never a guess. */
  a.stat {
    position: relative;
    padding-right: 2.3rem;
    border-color: #4a4370;
  }
  a.stat::after {
    content: "→";
    position: absolute;
    top: 1.55rem;
    right: 1rem;
    line-height: 1;
    color: var(--accent);
  }

  /* Add-a-thing forms (games, users, credit): the fields already fill the
     width, so the submit stops riding along at the end of the last field —
     it takes its own full-width row where a thumb expects it. Mirrors what
     the rip page's panel already does. */
  .card-entry input[type="text"],
  .card-entry input[type="email"] { flex: 1 1 100%; }
  .card-entry > button {
    flex: 1 1 100%;
    width: 100%;
    min-height: 44px;
  }

  /* Shipment cards: the pull list and the ship form each get the full width
     once they stack, instead of staying pinned to a 16rem column. */
  .shipment-body { gap: 0.9rem; }
  .shipment-body .pull-table { flex: 1 1 100%; min-width: 0; }
  .shipment-side { flex: 1 1 100%; }
  /* Buying postage and telling the buyer it shipped are the two most
     consequential taps in the app — full-width and thumb-sized, in a column
     that's already stacked. */
  .ship-form button { width: 100%; min-height: 44px; }
  .shipment-side .inline-form button.chip { min-height: 44px; }
  /* City / State / ZIP: three across is too narrow to read a city name at
     16px, three stacked makes the form long. City takes its own line, the
     two short fields share the next one. */
  .address-row { flex-wrap: wrap; }
  .address-row label { flex: 1 1 100%; }
  .address-row label.field-state,
  .address-row label.field-postal { flex: 1 1 8rem; }
  /* Address and Settings are two tabs of one section: the primary submit
     shouldn't change shape when you tap between them. Same treatment as
     .ship-form button — full width, thumb-sized. */
  .address-form button[type="submit"],
  .settings-form button[type="submit"] {
    width: 100%;
    min-height: 44px;
  }
  .address-form button[type="submit"] { margin-top: 0.2rem; }

  /* Legible floor for the small print. */
  .fineprint, .card-detail, .stat-label, .search-empty,
  .video-share, .ledger-row, .auth-alt, .field-hint, .rip-meta { font-size: 0.875rem; }

  /* The message box holds an example the buyer reads before typing; at 16px on
     a narrow column four rows cut it off. */
  .gift-form textarea { min-height: 8rem; }

  /* Dialog chrome gets a real hit box. */
  .share-close, .lb-x {
    display: grid;
    place-items: center;
    width: 44px;
    height: 44px;
    top: 0.4rem;
    right: 0.5rem;
  }
}

/* Rows that pack a thumb, a name, a price and an action into one line need
   two lines on a phone — the name column was being squeezed to nothing. */
@media (max-width: 560px) {
  .card-row { flex-wrap: wrap; gap: 0.55rem 0.8rem; }
  .card-main { flex: 1 1 8rem; }
  .card-value { align-items: flex-start; }
  .card-choice { flex: 1 1 100%; flex-wrap: wrap; }

  /* Pull rows: the name is the point, so it gets the whole line next to the
     thumb (60px + the 0.8rem gap = 4.55rem, rounded up), and the price and
     the Ship it / Credit pair share the line beneath it. Anything narrower
     and the name column was down to a word per line with the meta text
     running into the price. */
  .card-list .card-main { flex: 1 1 calc(100% - 4.75rem); }

  /* Once the row stacks there's height to spare, so the card you pulled
     stops being a 42px stamp — seeing it is the whole point of the page. */
  .card-list .card-thumb { width: 60px; }
  .card-list .card-value { flex: 0 1 auto; }
  .card-list .card-choice { flex: 1 1 auto; justify-content: flex-end; }

  /* Narrow phones: give the fields the room the card padding was eating. */
  .auth-card { padding: 1.4rem 1.25rem; }

  .rip-row { flex-wrap: wrap; }
  .rip-row-main { min-width: 0; flex: 1 1 12rem; }
  .rip-row-meta { flex-wrap: wrap; }

  /* The status is the whole point of the rips list, so on every phone width
     it takes its own full-width line — otherwise a short label sits inline
     and a long one wraps, and the rows jitter between two heights instead of
     forming a column you can scan. Same breakpoint the row wrap uses. */
  .rip-list .rip-row-meta {
    flex: 1 1 100%;
    min-width: 0;
    row-gap: 0.4rem;
  }
  .rip-list .rip-row-meta .status {
    white-space: normal;
    max-width: 100%;
    border-radius: 12px;
  }
}

/* Narrowest phones: keep the wordmark on one line. */
@media (max-width: 400px) {
  .wordmark { font-size: 1.15rem; letter-spacing: 0.02em; }
  .portal-label { font-size: 0.66rem; }
  main { width: min(960px, 100% - 1.6rem); }

  /* Narrow phones are short phones too, and the sub-headline wraps to another
     line here — the loop gives that line back. */
  .hero { padding-top: 1.25rem; }
  .hero-loop { max-height: 26vh; }

  /* On the narrowest phones the pinned name column gives back some room so
     there is still a usable window onto the rest of the catalog row. */
  .catalog-table th:first-child { width: 8rem; }
  .catalog-table td:first-child { max-width: 8.5rem; }

  /* Keep the ops tiles two across on a 320px phone — a column of one-number
     boxes is three screens of scrolling for nine numbers. */
  .stat-grid { grid-template-columns: repeat(auto-fill, minmax(128px, 1fr)); gap: 0.6rem; }
  .stat { padding: 0.85rem 1rem; }
  a.stat { padding-right: 2rem; }
  a.stat::after { top: 1.4rem; right: 0.8rem; }

}

/* The rip page on a phone: the two things a shipped buyer came for —
   tracking and the video — are thumb-sized, and the actions under the
   player stop reading as body copy. */
@media (max-width: 760px) {
  .track-link { min-height: 44px; flex: 1 1 auto; }
  /* A hover border was the only thing saying the chip goes somewhere, and a
     thumb never hovers — so next to a real Copy button it read as a value
     field and the buyer copied 22 digits by hand. The number wears the link
     colour and the chip carries a standing arrow, same cue as a.stat. */
  a.track-link { border-color: #4a4370; }
  a.track-link .track-number { color: var(--accent); text-decoration: underline; text-underline-offset: 3px; }
  a.track-link::after { content: "→"; color: var(--accent); line-height: 1; }
  a.track-link:active { border-color: var(--accent); }
  .chip.track-copy { min-height: 44px; padding: 0.35rem 1.1rem; font-size: 0.875rem; }
  /* The chip rule sets display, which would defeat [hidden] on the copy
     button before the script decides the clipboard is available. */
  .chip.track-copy[hidden] { display: none; }
  .video-actions .button-ghost {
    flex: 1 1 11rem;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    min-height: 44px;
  }
  .video-share-note, .rip-video-error { font-size: 0.875rem; }
}

/* The record page on a phone is one job: see the frame, hit the button. The
   page is a column the height of the viewport — heading, then the viewfinder
   taking every pixel that's left, then the shutter on the bottom edge. Sizing
   the frame to the leftover space instead of a slice of the screen means the
   dock can never sit on top of the picture, and there's no dead band under the
   controls.

   A phone held sideways is wider than 760px, so the short-and-wide case comes
   in on its own condition — a tilted phone is still a phone, and the shutter
   still has to be on the bottom edge. */
@media (max-width: 760px), (max-height: 560px) and (orientation: landscape) {
  /* The admin nav is four wrapped rows of chrome on a page you hold up to
     film. The breadcrumb is the way back. */
  .admin-shell:has(.record-page) .admin-nav { display: none; }

  /* svh, not vh: with the browser's chrome showing, vh is taller than the
     glass and the dock would hang below the fold. */
  body.admin:has(.record-page) { min-height: 100svh; }
  .admin-shell:has(.record-page) {
    display: flex;
    flex-direction: column;
    min-height: 0;
    /* The base shell is a grid with align-items:start, which as a flex column
       becomes a cross-axis "shrink to fit" — the viewfinder would be sized by
       the length of the heading text instead of by the phone (worst sideways,
       where the stage came out under half the screen). Stretch puts the frame
       back on the screen's edges. */
    align-items: stretch;
    /* Stated here rather than inherited from the phone-width shell rule, so a
       sideways phone (wider than 760px) gets the same frame. */
    width: min(1720px, 100% - 2rem);
    /* The shell's usual 4rem tail is dead space under a dock that wants the
       bottom edge. */
    padding: 1rem 0 0.5rem;
  }
  .record-page {
    display: flex;
    flex-direction: column;
    flex: 1 1 auto;
    min-height: 0;
  }

  .record-page h1 { font-size: 1.45rem; margin: 0.1rem 0 0.15rem; }
  .record-page > .fineprint { margin: 0 0 0.7rem; }
  /* Breadcrumbs are the only way off this page — thumb-sized. */
  .record-page .breadcrumbs { margin-bottom: 0.2rem; }
  .record-page .breadcrumbs a {
    display: inline-flex;
    align-items: center;
    min-height: 40px;
    padding: 0;
  }

  /* The stage is the flex-growing element: it takes whatever height the
     heading and the dock leave, full width, and the camera fills it. */
  .record-stage {
    flex: 1 1 auto;
    min-height: 180px;
    width: auto;
    max-width: none;
    margin-inline: 0;
    border-radius: var(--radius);
    background: #000;
    overflow: hidden;
  }
  .record-stage video {
    position: absolute;
    inset: 0;
    width: 100%;
    height: 100%;
    max-width: none;
    aspect-ratio: auto;
    border-radius: 0;
  }
  /* On the shortest phones the panel is close to the height of the stage, so
     it packs tighter and stays scrollable from the top if it ever isn't. */
  .record-alert {
    max-width: none;
    right: 0;
    width: auto;
    gap: 0.5rem;
    padding: 0.85rem;
    justify-content: safe center;
    overflow-y: auto;
  }
  .record-alert-body { font-size: 0.9rem; }

  .record-dock {
    position: sticky;
    bottom: 0;
    z-index: 5;
    flex: 0 0 auto;
    padding: 0.5rem 0 0.4rem;
    background: linear-gradient(to top, #1a1826 74%, rgba(26, 24, 38, 0));
  }
  .record-dock .record-controls { margin: 0 0 0.3rem; }
  .record-btn {
    flex: 1 1 auto;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    min-height: 54px;
    padding: 0.9rem 1rem;
  }
  .record-page #record-status { margin: 0; }
}

/* Sideways, the whole screen is about 390px tall. The heading block gives up
   its slack so the viewfinder keeps real height and the shutter still lands on
   the bottom edge instead of two screens down. */
@media (max-height: 560px) and (orientation: landscape) {
  .admin-shell:has(.record-page) { padding: 0.55rem 0 0.35rem; }
  .record-page h1 { font-size: 1.1rem; margin: 0; }
  .record-page > .fineprint { margin: 0 0 0.35rem; font-size: 0.8rem; }
  .record-page .breadcrumbs { margin-bottom: 0; }
  .record-stage { min-height: 110px; }
  /* The panel has to fit a box a third the height it has in portrait. */
  .record-alert { gap: 0.35rem; padding: 0.6rem 0.9rem; }
  .record-alert-title { font-size: 0.95rem; }
  .record-alert-body { font-size: 0.85rem; max-width: 34rem; }
  .record-alert-detail { font-size: 0.75rem; max-width: 34rem; }
  .record-alert button { padding: 0.5rem 1.2rem; min-height: 40px; }
  .record-dock { padding: 0.35rem 0 0.25rem; }
  .record-dock .record-controls { margin: 0 0 0.2rem; }
  .record-btn { min-height: 46px; padding: 0.6rem 1rem; font-size: 1.05rem; }
  .record-page #record-status { font-size: 0.78rem; }
  /* The gift note has to be read on camera, so it stays — just tighter, and
     capped so a long message can't push the shutter off the screen. */
  .record-page .gift-callout {
    padding: 0.45rem 0.7rem;
    margin-bottom: 0.4rem;
    font-size: 0.9rem;
    max-height: 5.5rem;
    overflow-y: auto;
  }
  .record-page .gift-callout-label { margin-bottom: 0.1rem; }
}

/* ---- an order's video tab ---- */

/* The chip is the action and the line under it is the explanation, so they
   sit closer to each other than to whatever follows. */
.video-action { margin: 1rem 0 0.5rem; }
.video-links { margin-bottom: 0.35rem; }

/* On a phone this tab is one of two jobs: attach a video, or act on the one
   that's already there. Both were built for a mouse. */
@media (max-width: 760px) {
  /* The URL and its submit each take a full row — "Save video" was an 80px
     stub wedged beside a field that already wanted the whole width. */
  .video-form { gap: 0.7rem; }
  .video-form label { flex: 1 1 100%; }
  .video-form button,
  .video-upload button {
    flex: 1 1 100%;
    width: 100%;
    min-height: 44px;
  }
  .video-upload label { margin-bottom: 0.7rem; }

  /* Chips here are controls you tap, not labels you read. Static span.chip
     status pills stay compact. */
  a.chip, button.chip { font-size: 0.875rem; }
  /* Removing a video deletes it from Cloudflare too — give the destructive
     one enough room that it's never a mis-tap. Its form is inline by default,
     so it can't hold a margin: on a phone it becomes its own block with real
     space under it, instead of reading as the first row of the attach form. */
  button.chip-danger { min-height: 44px; padding: 0.45rem 1.1rem; }
  .inline-form.video-remove { display: block; margin-bottom: 1rem; }

  /* Download / Open / Public page are the point of this tab once a rip
     lands, so they get thumb height instead of prose height. */
  .video-links { font-size: 0.9375rem; }
  .video-links a {
    display: inline-flex;
    align-items: center;
    min-height: 40px;
  }
}

/* ---- an order's cards tab ---- */

/* Seven columns of card detail never fit a phone. Scrolling the table
   sideways would hide the two things you open this tab for — the pick and
   Remove — so under 640px each row reflows into a small card instead:
   thumb on the left, name and market on the first line, set and number
   under it, then the pick, the HIT badge, and Remove on its own line. */
@media (max-width: 640px) {
  .order-cards,
  .order-cards tbody,
  .order-cards tr,
  .order-cards td {
    display: block;
  }
  .order-cards {
    overflow: visible;
    white-space: normal;
  }
  .order-cards thead { display: none; }
  .order-cards tr {
    display: grid;
    grid-template-columns: 34px minmax(0, 1fr) auto;
    column-gap: 0.7rem;
    row-gap: 0.25rem;
    align-items: center;
    padding: 0.75rem 0;
    border-bottom: 1px solid var(--line);
  }
  .order-cards td { padding: 0; border: 0; }
  /* Undecided picks and non-hits render empty cells; drop them so they
     don't leave a gap in the stack. Placement below is explicit, so
     removing a cell never shifts the others. */
  .order-cards td:empty { display: none; }

  .order-cards td:nth-child(1) { grid-column: 1; grid-row: 1 / span 3; align-self: start; }
  .order-cards td:nth-child(2) { grid-column: 2; grid-row: 1; font-weight: 600; }
  .order-cards td:nth-child(5) {
    grid-column: 3;
    grid-row: 1;
    text-align: right;
    white-space: nowrap;
  }
  .order-cards td:nth-child(3),
  .order-cards td:nth-child(4) { color: var(--muted); font-size: 0.875rem; }
  .order-cards td:nth-child(3) { grid-column: 2; grid-row: 2; }
  .order-cards td:nth-child(4) { grid-column: 3; grid-row: 2; text-align: right; white-space: nowrap; }
  .order-cards td:nth-child(4)::before { content: "#"; }
  .order-cards td:nth-child(6) { grid-column: 2; grid-row: 3; }
  .order-cards td:nth-child(7) { grid-column: 3; grid-row: 3; text-align: right; }
  /* Remove deletes the card off the customer's order — its own line, full
     thumb height, and clear of the pick chip beside it. */
  .order-cards td:nth-child(8) {
    grid-column: 2 / -1;
    grid-row: 4;
    justify-self: end;
    margin-top: 0.35rem;
  }
  .order-cards td:nth-child(8) button.chip-danger {
    min-height: 44px;
    padding: 0.45rem 1.2rem;
  }
}

/* Narrowest phones: four order tabs still scroll, but tighter gaps mean
   fewer of them start off screen. */
@media (max-width: 400px) {
  .order-tabs { gap: 1rem; }
}

/* ================================================================
   The rip page's admin panel — the card-entry station. Search a
   card, add the printing, mark the hits, attach the video, submit.
   All of it was laid out beside a mouse.
   ================================================================ */

/* "Find a card" is the first control on the panel and the only one whose
   label wasn't inside a <form>, so it never picked up the block label rule
   and sat inline beside a field it was supposed to introduce. */
.card-search label {
  display: block;
  margin: 0;
  font-weight: 500;
  font-size: 0.95rem;
}

/* The Add buttons carry a price. Kept as its own span so a phone can put it
   on a second line; the separator lives in CSS so the wide layout still
   reads "Add foil · $0.56". */
.chip-add { display: inline-flex; align-items: baseline; gap: 0.35rem; }
.chip-add .add-price { font-weight: 500; }
.chip-add .add-price::before { content: "·"; margin-right: 0.35rem; }

/* Remove deletes a card off the rip and sits inches from the Hit toggle.
   It says so without waiting for a hover a touchscreen never sends. */
.chip-danger { border-color: #5a2434; color: #ff9db0; }

@media (max-width: 760px) {
  /* ---- card search ---- */
  .card-search input[type="search"] { min-height: 44px; }
  .search-results { max-height: 60vh; }

  /* The row was one line of [thumb][name][Add][Add foil] with the actions
     refusing to shrink, so the name column collapsed to nothing and the
     buttons ran off the right edge of a list that only scrolls down. Now
     the card takes the first line and the actions the second. */
  .search-results li {
    flex-wrap: wrap;
    align-items: flex-start;
    gap: 0.5rem 0.7rem;
    padding: 0.6rem 0.8rem;
  }
  .search-results img { width: 44px; }
  .search-main { flex: 1 1 calc(100% - 3.5rem); }
  .search-actions {
    flex: 1 1 100%;
    justify-content: flex-start;
    gap: 0.5rem;
  }
  .search-actions .inline-form { display: flex; flex: 0 1 auto; }
  button.chip-add {
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: 0;
    min-height: 44px;
    padding: 0.35rem 0.9rem;
    line-height: 1.25;
    text-align: center;
  }
  button.chip-add .add-price { font-size: 0.8125rem; }
  button.chip-add .add-price::before { content: none; }

  /* ---- cards entered ---- */
  /* The card's name is what you check; the two buttons are what you press.
     They stop fighting over one line. */
  .admin-card-list li {
    flex-wrap: wrap;
    gap: 0.4rem 0.8rem;
    padding: 0.7rem 0;
  }
  .admin-card-list .card-detail { flex: 1 1 100%; }
  /* Hit is a toggle, Remove is permanent — a full gap between them, not the
     6px that made the delete a mis-tap. */
  .admin-card-list .card-choice {
    flex: 1 1 100%;
    justify-content: flex-start;
    gap: 1.5rem;
  }

  /* ---- manual entry ---- */
  /* `display: revert` puts back the disclosure triangle the shared
     inline-block rule dropped, and the padding makes the row thumb-sized. */
  .manual-entry summary {
    display: revert;
    min-height: 44px;
    padding: 0.7rem 0;
    margin-bottom: 0.4rem;
  }
  .admin-panel .card-entry { gap: 0.6rem; }
  .admin-panel .card-entry input[type="text"] { flex: 1 1 100%; }
  .admin-panel .card-entry .checkbox {
    flex: 1 1 40%;
    align-items: center;
    min-height: 44px;
    gap: 0.55rem;
    padding-right: 0.4rem;
  }
  .admin-panel .card-entry button { flex: 1 1 100%; width: 100%; min-height: 44px; }

  /* ---- submit ---- */
  /* Stacked on a phone, every button in this panel looked alike. This is the
     one that hands the rip to the buyer, so it gets its own weight and a
     line above it. */
  .submit-form {
    margin-top: 1.6rem;
    padding-top: 1.2rem;
    border-top: 1px solid var(--line);
  }
  .submit-form .button-primary {
    display: flex;
    align-items: center;
    justify-content: center;
    width: 100%;
    min-height: 48px;
  }
}

/* Narrowest phones: the dashed panel's own padding was eating the width the
   search rows needed. */
@media (max-width: 400px) {
  .admin-panel { padding: 1rem 0.9rem; }
  .search-results li { padding: 0.6rem 0.6rem; }
  .admin-panel .card-entry .checkbox { flex: 1 1 100%; }
}

/* ---- rip queue on a phone ---- */
/* Desktop keeps the console exactly as it was: the sort row and the spelled-out
   state are mobile-only additions. */
.queue-sort { display: none; }
.flow-state { display: none; }

@media (max-width: 760px) {
  /* Eight columns don't fit a phone and shouldn't try — the table scrolls in
     its own box. What that costs is identity: scrolled right to read Progress,
     every row looks alike. So the order column pins to the left edge. */
  .rip-queue th:first-child,
  .rip-queue td:first-child {
    position: sticky;
    left: 0;
    z-index: 1;
    background: var(--ink);
    box-shadow: 1px 0 0 var(--line);
  }

  /* The icon strip explains itself in title attributes, which a touch screen
     never shows. The plain state rides along under the icons instead. */
  .flow-state {
    display: block;
    margin-top: 0.15rem;
    font-size: 0.875rem;
    color: var(--muted);
  }

  /* Sorting lived in 13px column headers inside a table that pans under your
     thumb. Same sorts, thumb-sized, above the scroll — and the active one is
     marked, so the order is readable without the URL bar. */
  .queue-sort {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 0 1.1rem;
    margin: 1rem 0 0.2rem;
    font-size: 0.875rem;
  }
  .queue-sort-label { color: var(--muted); }
  .queue-sort a {
    display: inline-flex;
    align-items: center;
    min-height: 44px;
    text-decoration: none;
    border-bottom: 2px solid transparent;
  }
  .queue-sort a.on { color: var(--accent); border-bottom-color: var(--accent); }

  /* Headers still flip direction on a second tap — worth aiming at. */
  .rip-queue th a { padding: 0.75rem 0.4rem; margin: -0.6rem -0.4rem; }
  .flow-sort-hint a { display: inline-block; padding: 0.5rem 0; }
}

/* ================================================================
   Admin inventory, phone width. The foundation pass already makes
   dense admin tables scroll inside themselves; what's left is that
   Market / Order / Credited — the reason this page exists — start
   past the right edge with no sign they're there, and the order
   link is a two-character tap target.
   ================================================================ */

@media (max-width: 760px) {
  /* Edge shading that rides the table's own scroll: it shows on whichever
     side still has table, and fades out when you reach that end. */
  .inventory-table {
    background-color: var(--ink);
    background-image:
      linear-gradient(to right, var(--ink), rgba(20, 18, 31, 0)),
      linear-gradient(to left, var(--ink), rgba(20, 18, 31, 0)),
      linear-gradient(to right, rgba(0, 0, 0, 0.4), rgba(0, 0, 0, 0)),
      linear-gradient(to left, rgba(0, 0, 0, 0.4), rgba(0, 0, 0, 0));
    background-position: left center, right center, left center, right center;
    background-repeat: no-repeat;
    background-size: 22px 100%, 22px 100%, 12px 100%, 12px 100%;
    background-attachment: local, local, scroll, scroll;
  }

  /* The order number is the only way from a credited card back to the rip
     it came from, and "#4" is a 17px target. Thumb-sized instead. */
  .admin-table.inventory-table td a.order-ref {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    min-width: 44px;
    min-height: 44px;
    padding: 0 0.4rem;
    margin: 0;
  }
}

@media (max-width: 560px) {
  /* The art column reserves ~60px on every row — including rows with no
     image — and spends it pushing Market off the screen. The card name
     identifies the card; the art comes back on wider screens. */
  .inventory-table thead th:first-child,
  .inventory-table tbody td:first-child { display: none; }
  .admin-table.inventory-table td,
  .admin-table.inventory-table th { padding-left: 0.5rem; padding-right: 0.5rem; }
}

/* ---- rippers: the on-camera identity ---- */

.ripped-by { display: flex; align-items: center; gap: 0.45rem; color: var(--muted); font-size: 0.85rem; margin: 0.4rem 0 0; }
.ripped-by img { width: 26px; height: 26px; border-radius: 50%; object-fit: cover; border: 1px solid var(--line); }
.ripped-by a { color: var(--text); }

.ripper-head { display: flex; align-items: center; gap: 1.4rem; margin-bottom: 1rem; }
/* Ripper names are handles — a long single token would otherwise set a
   min-content floor wider than the phone and scroll the page sideways. */
.ripper-head > div { min-width: 0; }
/* break-word, not anywhere: it takes the hyphen in a double-barrelled name
   before it will split a word, so the name stays readable. */
.ripper-head h1 { margin: 0.1rem 0; overflow-wrap: break-word; hyphens: auto; }
.ripper-photo {
  width: 110px; height: 110px; border-radius: 50%; object-fit: cover;
  border: 2px solid var(--line); flex-shrink: 0;
}
.ripper-photo.placeholder {
  display: grid; place-items: center; background: var(--surface-2); color: var(--muted);
  font-family: "Unbounded", sans-serif; font-size: 2.6rem; font-weight: 900;
}
@media (max-width: 560px) {
  .ripper-head { gap: 0.9rem; }
  .ripper-photo { width: 72px; height: 72px; }
  .ripper-photo.placeholder { font-size: 1.7rem; }
  /* The global h1 clamp bottoms out at 1.6rem, which is still wider than a
     long handle gets on a phone. Names are the point of this page. */
  .ripper-head h1 { font-size: 1.35rem; }
}
/* Below ~400px the avatar and the name can't share a row without squeezing
   the name until it breaks mid-word. Stack them and it gets the full width. */
@media (max-width: 400px) {
  .ripper-head { flex-direction: column; align-items: flex-start; gap: 0.7rem; }
}
.ripper-bio { max-width: 44rem; white-space: pre-line; }

.ripper-thumb {
  width: 34px; height: 34px; border-radius: 50%; object-fit: cover;
  border: 1px solid var(--line); vertical-align: middle; display: inline-block;
}
.ripper-thumb.placeholder {
  display: inline-grid; place-items: center; background: var(--surface-2);
  color: var(--muted); font-weight: 700;
}
.ripper-chips { display: flex; flex-wrap: wrap; gap: 0.4rem; margin-bottom: 0.5rem; }
/* Avatar and name share the roster's identity cell so it can be the pinned
   column on a phone. */
.ripper-name-cell { display: flex; align-items: center; gap: 0.5rem; }

/* ---- the public roster ---- */

.roster-head { margin-bottom: 1.4rem; }
.roster-head h1 { margin-bottom: 0.3rem; }
/* One column on a phone, filling out as the screen allows — a roster is a
   short list, so cards stay wide enough to read rather than shrinking. */
.roster-grid {
  list-style: none; padding: 0; margin: 0;
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(min(100%, 17rem), 1fr));
  gap: 0.8rem;
}
.roster-card {
  display: flex; align-items: center; gap: 0.9rem;
  padding: 0.85rem;
  background: var(--surface); border: 1px solid var(--line);
  border-radius: var(--radius);
  text-decoration: none; color: inherit;
  /* The whole card is the tap target, never just the name. */
  min-height: 5rem;
}
.roster-card:hover { border-color: var(--accent); }
.roster-photo {
  width: 64px; height: 64px; flex-shrink: 0;
  border-radius: 50%; object-fit: cover; border: 1px solid var(--line);
}
.roster-photo.placeholder {
  display: grid; place-items: center; background: var(--surface-2);
  color: var(--muted); font-family: "Unbounded", sans-serif;
  font-size: 1.6rem; font-weight: 900;
}
.roster-body { display: flex; flex-direction: column; gap: 0.15rem; min-width: 0; }
.roster-name { font-weight: 700; }
/* Long taglines truncate instead of pushing the card wider than the phone. */
.roster-body .card-detail { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.roster-meta { display: flex; align-items: center; gap: 0.5rem; flex-wrap: wrap; }
.form-error {
  background: color-mix(in srgb, #ff8b8b 12%, transparent);
  border: 1px solid color-mix(in srgb, #ff8b8b 40%, transparent);
  border-radius: 8px; padding: 0.6rem 0.9rem; font-size: 0.9rem;
}

/* ---- rip names + dedications ---- */

.rip-name { color: var(--muted); font-size: 0.9rem; font-style: italic; margin: 0.2rem 0 0; }
.ripname-callout strong { color: var(--accent); }
.dedication-details { margin-bottom: 0.8rem; }
.dedication-details summary { cursor: pointer; color: var(--muted); font-size: 0.9rem; margin-bottom: 0.5rem; }
.dedication-details label { font-size: 0.9rem; }
.buy-form { display: block; }
.dedication-form { max-width: 26rem; margin: 0.8rem 0; }
.dedication-form label { font-size: 0.9rem; }

/* Order Overview: the customer and this-order panels. Two across on a
   desk, stacked on a phone; the definition list keeps labels in a narrow
   column so values line up. */
.detail-panels { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(20rem, 100%), 1fr)); gap: 1rem 2rem; margin: 1.5rem 0 1rem; }
.detail-panel .section-label { margin: 0 0 0.5rem; }
.detail-list { display: grid; grid-template-columns: max-content 1fr; gap: 0.35rem 1rem; margin: 0; align-items: baseline; }
.detail-list dt { color: var(--muted); font-size: 0.85rem; }
.detail-list dd { margin: 0; min-width: 0; overflow-wrap: anywhere; }
.detail-list code { font-size: 0.85em; }
.detail-list .ship-to { margin: 0; }
@media (max-width: 899px) {
  .detail-list { grid-template-columns: 1fr; gap: 0.1rem 0; }
  .detail-list dt { margin-top: 0.5rem; }
}

/* The label form on the order's Fulfillment tab: package chips (radios
   dressed as chips), one row of weight + the rigid toggle, one row of the
   two buys, then the rates table when asked for. */
.label-form { display: flex; flex-direction: column; gap: 0.5rem; margin: 0.6rem 0; }
.package-picks { display: flex; flex-wrap: wrap; gap: 0.4rem; }
.package-pick input { position: absolute; opacity: 0; width: 0; height: 0; }
.package-pick:has(input:focus-visible) { outline: 2px solid var(--accent); outline-offset: 2px; }
.package-hint { margin: 0; }
.label-row { display: flex; flex-wrap: wrap; gap: 0.6rem 1.2rem; align-items: center; }
.label-row label { display: flex; align-items: center; gap: 0.4rem; margin: 0; }
.label-row label[hidden] { display: none; } /* display:flex would otherwise beat [hidden] */
.label-row input[type=number] { width: 6rem; }
.rates { display: flex; flex-direction: column; gap: 0.4rem; margin: 0.4rem 0 0.8rem; }
.rates-table td { padding: 0.35rem 0.6rem; vertical-align: middle; }
.rates-table td:first-child { padding-left: 0; }
.rates-table .num { text-align: right; font-variant-numeric: tabular-nums; }

.label-sizes { display: flex; flex-direction: column; gap: 0.5rem; }
.settings-form .label-size { flex-direction: row; align-items: baseline; gap: 0.5rem; font-weight: 400; }
.settings-form .label-size input { width: auto; }
.settings-form .label-size strong { white-space: nowrap; }
/* Phone + email split evenly; the state/zip row's narrow first column
   would squeeze a phone number. */
.settings-form .settings-row-even { display: grid; grid-template-columns: 1fr 1fr; gap: 0.6rem; }

.ship-form .queue-rigid { flex-direction: row; align-items: center; gap: 0.4rem; font-weight: 400; }
.ship-form .queue-rigid[hidden] { display: none; }
