:root {
  --mo-accent: #2c7a7b;                  /* teal — music/album semantic color */
  /* Charter palette + role accent — OPERATOR (dev) defaults: warm.
     spec-musicorgan-operator-player-organ-banner. .mo-player overrides below.
     Role (warm/cool) is ORTHOGONAL to theme (light/dark): role drives the accent
     + mascot skin only; banner/surface darkness is theme-driven (tokens below). */
  --charter-body: #ff905f;
  --charter-shade:#e76a39;               /* deeper skin tone for the blend */
  --charter-arm:  #ffaf8f;
  --charter-accent:#4aa3ff;              /* opposite-hue accent — blue weaves through the orange */
  --charter-eye:  #ffffff;
  --charter-pupil:#2b3b47;
  --mo-role-accent: #ff8c42;             /* warm — Operator */

  /* ===== Style system — semantic tokens (spec-musicorgan-style-system) =====
     LIGHT is the default theme (operator preference: light, CTAs not dark).
     [data-bs-theme="dark"] below restores the prior dark mood. Every bespoke
     color in this sheet should reference a token; no new raw hex outside these
     two blocks. Stock Bootstrap components follow data-bs-theme on their own. */
  --mo-surface:    #f6f8fb;              /* page background */
  --mo-surface-2:  #ffffff;              /* cards / elevated panels */
  --mo-surface-3:  #eef2f7;             /* inset / row hover */
  --mo-border:     #d8e0ea;
  --mo-text:       #1a202c;
  --mo-text-muted: #5b6b7f;
  --mo-banner-bg:    linear-gradient(#ffffff, #eef2f7);
  --mo-banner-text:  #1a202c;
  --mo-banner-muted: #5b6b7f;
  --mo-cta-bg:       #ffffff;            /* LIGHT cta button — not a dark fill */
  --mo-cta-text:     #1a202c;
  --mo-cta-border:   #cbd5e0;
  --mo-cta-hover-bg: #eef2f7;
  --mo-stage-bg:     #eef2f7;            /* now-playing stage */
  --mo-stage-text:   #1a202c;
  --mo-stage-muted:  #5b6b7f;
}
/* PLAYER (Fly public read-only): cool/blue palette + accent. Role only — no
   banner/theme color here (that moved to the theme tokens above). */
.mo-player {
  --charter-body: #5aa9ff;
  --charter-shade:#2f7fd6;               /* deeper skin tone for the blend */
  --charter-arm:  #9fccff;
  --charter-accent:#ff8a4c;              /* opposite-hue accent — orange weaves through the blue */
  --charter-eye:  #ffffff;
  --charter-pupil:#16263a;
  --mo-role-accent: #38bdf8;             /* cool — Player */
}
/* DARK theme — restores the prior dark mood as an opt-in via the banner toggle. */
[data-bs-theme="dark"] {
  --mo-surface:    #0f1626;
  --mo-surface-2:  #1a202c;
  --mo-surface-3:  #222a38;
  --mo-border:     rgba(255,255,255,.12);
  --mo-text:       #e2e8f0;
  --mo-text-muted: #94a3b8;
  --mo-banner-bg:    linear-gradient(#20283a, #1a202c);
  --mo-banner-text:  #f1f5f9;
  --mo-banner-muted: rgba(255,255,255,.6);
  --mo-cta-bg:       rgba(255,255,255,.06);
  --mo-cta-text:     rgba(255,255,255,.92);
  --mo-cta-border:   rgba(255,255,255,.18);
  --mo-cta-hover-bg: rgba(255,255,255,.14);
  --mo-stage-bg:     #1a202c;
  --mo-stage-text:   #e2e8f0;
  --mo-stage-muted:  #cbd5e0;
}
body { background: var(--mo-surface); color: var(--mo-text); }
.navbar-brand i { color: var(--mo-accent); }
/* banner text reads from theme tokens (we drop Bootstrap's navbar-dark) */
.mo-banner .navbar-brand, .mo-banner .navbar-brand .fw-bold,
.mo-banner .nav-link, .mo-banner .navbar-toggler { color: var(--mo-banner-text); }
.mo-banner .navbar-toggler { border-color: var(--mo-border); }
.mo-theme-toggle { color: var(--mo-banner-text); background: transparent; border: 1px solid var(--mo-border); }
.mo-theme-toggle:hover, .mo-theme-toggle:focus-visible { color: var(--mo-role-accent); border-color: var(--mo-role-accent); }

/* ---- banner + role badge ---- */
.mo-banner { background: var(--mo-banner-bg); }
.mo-role-badge {
  display:inline-flex; align-items:center; gap:.3rem;
  font-size:.68rem; font-weight:700; text-transform:uppercase; letter-spacing:.06em;
  padding:.12rem .55rem; border-radius:1rem; margin-left:.55rem; color:#fff;
  background: var(--mo-role-accent); border:1px solid rgba(255,255,255,.28);
  white-space:nowrap;
}

/* ---- operator catalog switcher (spec-musicorgan-operator-catalog-switcher) ---- */
/* A compact navbar selector sitting beside the role badge: shows the ACTIVE
   catalog's label; the dropdown lists the operator's choices grouped by archive,
   with the active one checked. Operator-only (the includer guards on !read_only). */
.mo-catalog-switch .dropdown-toggle {
  font-size:.72rem; font-weight:600; max-width:14rem;
  border-color: var(--mo-border); color: var(--mo-banner-text);
}
.mo-catalog-switch .mo-catalog-active-label {
  max-width:9rem; overflow:hidden; text-overflow:ellipsis; white-space:nowrap;
}
.mo-catalog-menu { max-height:70vh; overflow-y:auto; min-width:16rem; }
.mo-catalog-menu .dropdown-item.active,
.mo-catalog-menu .dropdown-item.active:focus {
  background: var(--mo-role-accent); color:#fff;
}
.mo-catalog-menu form { display:block; }

/* ---- Charter mascot: palette via CSS vars + idle animation ---- */
.charter { width: 2.6rem; height: 2.6rem; display:block; overflow:visible; }
/* hero variant on the Charter home: large, centered, soft drop shadow */
.charter--hero { width: clamp(8rem, 22vw, 13rem); height: clamp(8rem, 22vw, 13rem);
  margin-inline:auto; filter: drop-shadow(0 10px 20px rgba(0,0,0,.18)); }
.mo-role-badge--hero { font-size:.8rem; padding:.3rem .85rem; margin-left:0; }
/* brand lockup tagline: "Music Organ" sitting under the Charter wordmark.
   Muted by default (light login card); brightened on the dark banner. */
.mo-tagline {
  font-size:.62rem; font-weight:600; line-height:1;
  text-transform:uppercase; letter-spacing:.08em;
  color: var(--mo-banner-muted);
}
/* body wears the living-skin gradient (animated stop-colors below) + mottle filter */
.charter-body  { fill: url(#charter-skin-grad); }
.charter-arm   { fill: var(--charter-arm); }
/* chromatophore skin: 4 stops drift between the role's skin tones AND the
   opposite-hue accent, desynced — blues and oranges weave across the body.
   Stop 3 anchors the accent so the second hue is always visible. */
.charter-stop--1 { stop-color: var(--charter-shade);  animation: charter-skin-1 9s  ease-in-out infinite; }
.charter-stop--2 { stop-color: var(--charter-body);   animation: charter-skin-2 11s ease-in-out infinite; }
.charter-stop--3 { stop-color: var(--charter-accent); animation: charter-skin-3 8s  ease-in-out infinite; }
.charter-stop--4 { stop-color: var(--charter-arm);    animation: charter-skin-4 6.5s ease-in-out infinite; }
@keyframes charter-skin-1 { 0%,100% { stop-color: var(--charter-shade); }  50% { stop-color: var(--charter-accent); } }
@keyframes charter-skin-2 { 0%,100% { stop-color: var(--charter-body); }   50% { stop-color: var(--charter-arm); } }
@keyframes charter-skin-3 { 0%,100% { stop-color: var(--charter-accent); } 50% { stop-color: var(--charter-body); } }
@keyframes charter-skin-4 { 0%,100% { stop-color: var(--charter-arm); }    50% { stop-color: var(--charter-shade); } }
.charter-eye   { fill: var(--charter-eye); }
.charter-pupil-fill { fill: var(--charter-pupil); }
/* gentle whole-mascot sway — reads as arms drifting left/right + up/down */
.charter-arms { transform-box: fill-box; transform-origin: 50% 35%; animation: charter-sway 6s ease-in-out infinite; }
@keyframes charter-sway {
  0%   { transform: rotate(-2.6deg) translateY(0); }
  50%  { transform: rotate(2.6deg)  translateY(-12px); }
  100% { transform: rotate(-2.6deg) translateY(0); }
}
/* pupils dart on independent timings — the two eyes do NOT move together */
.charter-pupil { transform-box: fill-box; }
.charter-pupil--l { animation: charter-dart-l 5.3s ease-in-out infinite; }
.charter-pupil--r { animation: charter-dart-r 7.1s ease-in-out infinite; }
@keyframes charter-dart-l {
  0%,18%   { transform: translate(0,0); }
  26%,40%  { transform: translate(8px,-4px); }
  52%,66%  { transform: translate(-6px,5px); }
  78%,100% { transform: translate(5px,1px); }
}
@keyframes charter-dart-r {
  0%,12%   { transform: translate(-5px,3px); }
  30%,44%  { transform: translate(7px,4px); }
  58%,72%  { transform: translate(3px,-5px); }
  84%,100% { transform: translate(-4px,0); }
}
/* conductor's baton: the raised arm holds a baton; the conducting WAVE is the idle
   animation. The baton tapers to a point (its own taper = the tapered tip) and
   waves in free space beyond the arm, so no masking/blunting of the octopus arm.
   Pivots at the grip, desynced from the body sway / eye darts / skin.
   spec-musicorgan-charter-arm-wiggle */
/* plain uniform rod: a dark edge under a white core, rounded tip. NOT tapered —
   the octopus arm keeps its own natural smooth tip (that was what blunted earlier). */
.charter-baton-shaft-edge { fill: none; stroke: rgba(35,24,12,.55); stroke-width: 4.4; stroke-linecap: round; }
.charter-baton-shaft      { fill: none; stroke: #fbfaf5;          stroke-width: 3.0; stroke-linecap: round; }
/* classic dark cork handle (not themed — role is signalled elsewhere) */
.charter-baton-grip  { fill: #241d15; stroke: rgba(15,10,5,.55); stroke-width: 1; }
.charter-baton {
  transform-box: fill-box; transform-origin: 16% 90%;
  animation: charter-baton-wave 2.8s cubic-bezier(0.4,0,0.2,1) infinite;
}
@keyframes charter-baton-wave {
  0%   { transform: rotate(-13deg); }
  50%  { transform: rotate(13deg); }
  100% { transform: rotate(-13deg); }
}
@media (prefers-reduced-motion: reduce) {
  .charter-arms, .charter-pupil--l, .charter-pupil--r, .charter-baton,
  .charter-stop--1, .charter-stop--2, .charter-stop--3, .charter-stop--4 { animation: none !important; }
}
/* dev inspect aid — append ?inspect=armtip to the URL. Outlines the baton in cyan
   and slows the conducting wave (SAME ±13deg, longer 9s period) so the motion is
   easy to follow on the live page. Gated on <html class="inspect-armtip">; never
   affects the default render. spec-clodi-leaning-motion-tunicate. */
.inspect-armtip .charter-baton-shaft { stroke: #00e5ff; }
.inspect-armtip .charter-baton-grip  { stroke: #00e5ff; stroke-width: 2.5; }
.inspect-armtip .charter-baton { animation-duration: 9s; }
.inspect-armtip body::after {
  content: "INSPECT · cyan = .charter-baton (conductor's baton) · real ±13° conducting wave, slowed to 9s · drop ?inspect=armtip to restore";
  position: fixed; left: 0; right: 0; bottom: 0; z-index: 2000;
  background: #00121a; color: #00e5ff; text-align: center;
  font: 600 .78rem/1.7 system-ui, sans-serif; letter-spacing: .02em; padding: .35rem;
}

/* ---- organ manual: large black & white keys, hover-depress ---- */
/* navbar-expand-lg forces flex-wrap:nowrap at >=992px, which squeezed the keys
   onto the content row. Make the sticky banner a plain BLOCK stack instead:
   the inner .container-fluid keeps Bootstrap's own display:flex (brand/search
   row), and .mo-keys + #mo-progress fall below it full-width. A block sticky
   element avoids the flex-direction:column + position:sticky resize glitch
   (banner drifting down, whitespace above). */
.mo-banner.navbar { display: block; }
/* no hamburger (spec-musicorgan-banner-no-hamburger): the banner row stays inline
   at every width, so let it wrap on narrow screens instead of overflowing. */
.mo-banner > .container-fluid { flex-wrap: wrap; row-gap: .3rem; }
.mo-keys { position: relative; width: 100%; height: 24px; user-select: none; border-top: 2px solid var(--mo-role-accent); }
.mo-keys-white { display: flex; height: 100%; }
.mo-key-w {
  flex: 1 1 0; background: linear-gradient(#fff, #ededed);
  border-right: 1px solid #b9b9b9; border-bottom: 2px solid rgba(0,0,0,.28);
  transform-origin: top; transition: transform .08s ease, background .08s ease;
}
.mo-key-w:last-child { border-right: 0; }
.mo-key-w:hover { background: linear-gradient(var(--mo-role-accent), #fff); transform: scaleY(.9); border-bottom-width: 1px; }
.mo-keys-black { position: absolute; inset: 0 0 auto 0; height: 62%; pointer-events: none; }
.mo-key-b {
  position: absolute; top: 0; width: 4.3%; height: 100%;
  background: linear-gradient(#3a3a3a, #050505); border-radius: 0 0 3px 3px;
  box-shadow: 0 2px 2px rgba(0,0,0,.5); pointer-events: auto;
  transform-origin: top; transition: transform .08s ease, background .08s ease;
}
.mo-key-b:hover { background: linear-gradient(var(--mo-role-accent), #1a1a1a); transform: scaleY(.88); }
.mo-key-b:nth-child(1) { left: 4.99%; }
.mo-key-b:nth-child(2) { left: 12.14%; }
.mo-key-b:nth-child(3) { left: 26.42%; }
.mo-key-b:nth-child(4) { left: 33.56%; }
.mo-key-b:nth-child(5) { left: 40.71%; }
.mo-key-b:nth-child(6) { left: 54.99%; }
.mo-key-b:nth-child(7) { left: 62.14%; }
.mo-key-b:nth-child(8) { left: 76.42%; }
.mo-key-b:nth-child(9) { left: 83.56%; }
.mo-key-b:nth-child(10){ left: 90.71%; }
.table-hover tbody tr:hover { background: #f0fbfb; }
.count-badge { font-variant-numeric: tabular-nums; }

/* htmx in-flight bar under the navbar */
.mo-progress { height: 2px; background: transparent; }
.mo-progress.htmx-request { background: linear-gradient(90deg, var(--mo-accent), #4fd1c5); animation: mo-pulse 1s ease-in-out infinite; }
@keyframes mo-pulse { 0%,100% { opacity:.4 } 50% { opacity:1 } }

/* ---- persistent player bar (now a static row inside the sticky top header) ---- */
.mo-topbar { z-index: 1030; background: var(--mo-surface); }  /* banner + Now Playing pin together at top; OPAQUE so the scrolling list never bleeds through (spec-musicorgan-now-playing-opaque-header) */
/* top-of-list == bottom of the player: any scrollIntoView/anchor jump lands the
   target BELOW the sticky header, not hidden behind the Now-Playing widget.
   --mo-topbar-h is measured live in mo-player.js (trackTopbarHeight). */
html { scroll-padding-top: var(--mo-topbar-h, 0px); }
/* now-playing stage — scoped to #mo-player so these layout/color rules never
   bleed onto body.mo-player (the role class shares the name). Themed via stage
   tokens; the dark-stage utility classes inside are re-skinned just below. */
#mo-player {
  background: var(--mo-stage-bg); color: var(--mo-stage-text);
  min-height: 20vh; display: flex; flex-direction: column; justify-content: center;
  gap: .35rem; padding-block: .6rem;
}
/* re-skin the player bar's Bootstrap dark-stage utilities through tokens so the
   stage follows the theme without editing the template's utility classes. */
#mo-player .btn-outline-light { color: var(--mo-stage-text); border-color: var(--mo-border); }
#mo-player .btn-outline-light:hover, #mo-player .btn-outline-light:focus-visible {
  background: var(--mo-role-accent); border-color: var(--mo-role-accent); color: #fff;
}
#mo-player .text-light { color: var(--mo-stage-text) !important; }
#mo-player .text-secondary { color: var(--mo-stage-muted) !important; }
.mo-player-stage { width: 100%; }
/* big control satellites around the now-playing portlet */
.mo-pbtn { font-size: 1.5rem; line-height: 1; padding: .35rem .6rem; border-radius: .6rem; }
.mo-pbtn-lg { font-size: 2.4rem; padding: .2rem .75rem; border-radius: 50%; }
.mo-pbtn .badge { font-size: .6rem; }
/* now-playing portlet (centre): big art + big title */
.mo-portlet { min-width: 0; max-width: 42vw; cursor: pointer; }
.mo-portlet-art { height: clamp(72px, 13vh, 132px); width: clamp(72px, 13vh, 132px); object-fit: cover; background:#e6fffa; }
.mo-portlet-title { font-size: 1.4rem; font-weight: 600; line-height: 1.2; }
.mo-portlet-sub { font-size: 1.05rem; }
.mo-status-line { font-size: .82rem; opacity: .85; }
/* THE single control bar (spec-musicorgan-single-unified-player-transport):
   headless engine renders nothing; the transport + scrubber + time + volume ARE
   its UI. The native <audio controls> bar (the old "second player") is gone. */
.mo-audio-headless { display: none; }
/* full-width scrubber row under the stage */
#mo-player .mo-seek {
  flex: 1 1 auto; width: auto; min-width: 120px; max-width: none;
  height: 6px; accent-color: var(--mo-role-accent); cursor: pointer; margin: 0;
}
.mo-time { font-variant-numeric: tabular-nums; font-size: .9rem; white-space: nowrap; }
/* LEFT zone: transport stacked over volume; the slider fills the transport width */
.mo-player-center { min-width: 0; }      /* let the card title truncate inside flex */
#mo-player .mo-vol { width: 124px; accent-color: var(--mo-role-accent); cursor: pointer; margin: 0; }
#mo-player .mo-volume .mo-pbtn { font-size: 1.1rem; padding: .2rem .4rem; }
/* RIGHT zone — queue / favourite / locate / info, sitting to the right of the
   now-playing card (spec-musicorgan-single-unified-player-transport). Spatially
   separated, so no divider needed. */
.mo-player-utils .mo-pbtn { font-size: 1.2rem; padding: .25rem .45rem; }
/* absorbed album card — filled when the on-screen album IS the one playing; the
   AlbumView hero is suppressed so it shows once, in the player. */
.mo-now-album { max-width: 34vw; padding-left: 1rem; border-left: 1px solid var(--mo-border); }
.mo-now-album-title { font-size: 1.15rem; }
.mo-now-album-sub { font-size: 1.05rem; }
/* !important: the hero keeps Bootstrap's .d-flex (display:flex !important), which
   would otherwise win (leaning-bootstrap-d-flex-overrides-hidden-attr). */
body.mo-merged #mo-av-header { display: none !important; }

/* MERGED layout (Kris 2026-06-14, spec-musicorgan-player-absorbs-album-merge amend):
   the absorbed album block STACKS ABOVE the now-playing track (not side-by-side).
   #mo-now-album sits on top (order:-1) carrying album title·year·id·Play album; the
   track portlet renders below. order: keeps the DOM (clone target last) but paints
   album first. Drop the side divider; left-align the stack. */
body.mo-merged .mo-player-center { flex-direction: column; align-items: flex-start; gap: .35rem; }
body.mo-merged #mo-now-album { order: -1; max-width: 100%; padding-left: 0; border-left: 0; }
body.mo-merged .mo-now-album-line { white-space: normal; }

/* ---- COMPACT player (spec-musicorgan-player-card-reduce-enlarge-contextual-shrink) ----
   GET_SMALL: a slim single row for browsing/lab. Keeps art thumb + title + core
   transport (prev/play/next) + scrubber/time + the reduce/enlarge toggle. Hides
   the volume stack, the utils cluster, the absorbed-album card, and the queue
   drawer; the queue toggle stays (collapsed to its icon) so up-next is reachable.
   The headless #mo-audio engine is untouched — playback stays gapless. */
.mo-size-toggle { font-size: 1.15rem; }
/* smooth height/padding transition between sizes; motion-safe only */
#mo-player { transition: min-height .22s ease, padding-block .22s ease; }
@media (prefers-reduced-motion: reduce) {
  #mo-player { transition: none; }
}
#mo-player.mo-compact {
  min-height: 0;
  padding-block: .3rem;
  gap: .15rem;
}
/* COMPACT ROW ORDER (Kris 2026-06-27): transport -> now-playing card (hugging
   the transport) -> flex spacer -> reduce/enlarge toggle pinned to the row's
   FAR RIGHT. The full layout's justify-content-between would otherwise shove the
   card to the right edge (big gap, card far away); in compact we pack left
   (flex-start) so the card sits IMMEDIATELY after the transport, then lift the
   size-toggle out of the transport flow and anchor it absolutely to the stage's
   right edge. position-relative scopes the absolute toggle to the stage. The
   toggle stays in .mo-transport in the DOM (full layout untouched, JS finds it
   by #id), it's only re-positioned visually here. */
#mo-player.mo-compact .mo-player-stage {
  gap: .5rem;
  justify-content: flex-start;
  position: relative;
  /* reserve room on the right so the now-playing card truncates before it
     runs under the absolutely-positioned toggle */
  padding-right: 3rem;
}
/* pin the toggle to the far-right edge of the slim row, vertically centred */
#mo-player.mo-compact .mo-player-stage .mo-size-toggle {
  position: absolute;
  right: .75rem;
  top: 50%;
  transform: translateY(-50%);
  margin: 0;
  z-index: 2;
}
/* hide the heavy zones — volume stack, right-side utils, absorbed album card */
#mo-player.mo-compact .mo-volume,
#mo-player.mo-compact .mo-player-utils,
#mo-player.mo-compact #mo-now-album { display: none !important; }
/* the absorbed-album merge stacks the centre column — in compact, force the
   single-row card back (the album block is hidden anyway) */
body.mo-player-compact.mo-merged .mo-player-center { flex-direction: row; align-items: center; }
/* shrink the now-playing portlet to a slim thumb + one-line title */
#mo-player.mo-compact .mo-portlet-art { height: 40px; width: 40px; }
#mo-player.mo-compact .mo-portlet-title { font-size: 1rem; }
#mo-player.mo-compact .mo-portlet-sub { font-size: .82rem; }
/* drop the secondary readouts that eat vertical space in compact */
#mo-player.mo-compact #mo-now-album-id,
#mo-player.mo-compact .mo-status-line { display: none; }
/* tighten the scrubber row */
#mo-player.mo-compact .mo-player-controls { padding-bottom: .25rem; }
#mo-player.mo-compact .mo-time { font-size: .8rem; }
/* smaller transport satellites for the slim row */
#mo-player.mo-compact .mo-pbtn { font-size: 1.2rem; padding: .2rem .45rem; }
#mo-player.mo-compact .mo-pbtn-lg { font-size: 1.7rem; padding: .15rem .55rem; }
/* The queue toggle (with its count badge) STAYS in compact so up-next is still
   reachable; the drawer keeps its default hidden state and opens on demand via
   MO.toggleDrawer() — we don't force it shut, only let it not occupy space. */

/* OWL · Metadata operator card (spec-musicorgan-owl-metadata-operator-card) */
.mo-owl-card .card-header { background: var(--mo-surface-2, rgba(0,0,0,.03)); }
.mo-owl-flabel { width: 5.5rem; flex: 0 0 5.5rem; }
.mo-owl-input { max-width: 24rem; }
.mo-owl-genre-add { width: 9rem; }
.mo-owl-genre-x { cursor: pointer; color: inherit; line-height: 1; }
.mo-owl-field + .mo-owl-field { border-top: 1px dashed var(--mo-border); }
/* click-to-edit fields: value reads as plain text, hints editable on hover */
.mo-owl-display { cursor: text; border-bottom: 1px dashed transparent; padding: 0 1px; }
.mo-owl-display:hover { border-bottom-color: var(--mo-border); }
.mo-owl-row .mo-owl-input { max-width: 22rem; min-width: 8rem; }
.mo-owl-overdot { font-size: .72rem; }
.mo-owl-altlabel { white-space: nowrap; }
/* Narrow phones (spec-musicorgan-narrow-header-shrink): shrink the banner +
   player chrome so the header stops wrapping onto extra rows. Drop the
   Operator/Player badge, trim the brand mark, and make the player control
   satellites + portlet smaller — reduce button size rather than line-wrap. */
@media (max-width: 480px) {
  .mo-role-badge { display: none; }
  /* Catalog badge keeps the album count but drops the '· updated Xm ago' tail
     on narrow phones — shrink, don't wrap the header
     (spec-musicorgan-narrow-header-shrink / -banner-no-hamburger). */
  .mo-catalog-ago { display: none; }
  .charter { width: 2.1rem; height: 2.1rem; }
  #mo-player .mo-player-stage { gap: .5rem; }
  #mo-player .mo-player-stage > .d-flex { gap: .3rem; }
  .mo-pbtn { font-size: 1.15rem; padding: .2rem .4rem; }
  .mo-pbtn-lg { font-size: 1.55rem; padding: .15rem .5rem; }
  .mo-portlet-art { height: 48px; width: 48px; }
  .mo-portlet-title { font-size: 1.05rem; }
  .mo-portlet-sub { font-size: .82rem; }
  .mo-now-album { max-width: 60vw; padding-left: .5rem; }
  .mo-now-album-title { font-size: .95rem; }
  .mo-now-album-sub { font-size: .82rem; }
  .mo-player-utils { padding-left: .4rem; }
  /* keep the single control bar on ONE row — shrink rather than wrap: drop the
     volume slider (mute button stays), tighten the scrubber + time. */
  #mo-player .mo-volume .mo-vol { display: none; }
  #mo-player .mo-seek { min-width: 64px; }
  .mo-time { font-size: .78rem; }
}
/* current-track ID reveal under the strip (own classes; not the canonical mo-id-chip) */
.mo-now-id { display: flex; }
.mo-nowid-text { color: var(--mo-stage-muted); }
.mo-nowid-copy.mo-nowid-copied { color: #34d399; border-color: #34d399; }
/* "show in list" locate flash on the target row */
.mo-row-flash { animation: mo-row-flash 1.3s ease-out; }
@keyframes mo-row-flash { 0%,25% { background: #ffe9a8 !important; } 100% { background: transparent; } }
@media (prefers-reduced-motion: reduce) { .mo-row-flash { animation: none !important; } }
.mo-now { min-width: 0; }
.mo-now-art { height: 38px; width: 38px; object-fit: cover; }
/* album-art thumbnails in cards/grids */
.mo-art { object-fit: cover; background: #e6fffa; }
.mo-art-card { width: 56px; height: 56px; flex: 0 0 56px; }
.mo-art-detail { width: 72px; height: 72px; }
#mo-player .btn-outline-light.active { background: var(--mo-accent); border-color: var(--mo-accent); color:#fff; }

/* up-next drawer */
.mo-queue-drawer { background:var(--mo-surface-3); color:var(--mo-stage-text); max-height: 45vh; overflow-y:auto; }
.mo-queue-list li { display:flex; align-items:center; gap:.5rem; padding:.25rem .75rem; cursor:pointer; border-bottom:1px solid var(--mo-border); }
.mo-queue-list li:hover { background:var(--mo-surface-2); }
.mo-queue-list li.mo-current { background:#2c7a7b33; }
.mo-queue-list li.mo-current .mo-q-title { color:var(--mo-accent); font-weight:600; }
.mo-queue-list .mo-q-title { flex:1; min-width:0; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
.mo-queue-list .mo-q-sub { color:var(--mo-stage-muted); font-size:.8em; }
.mo-queue-list .mo-q-rm { color:var(--mo-stage-muted); }
.mo-queue-list .mo-q-rm:hover { color:#f56565; }

/* ---- NESTING (spec-musicorgan-cloud-player-client-prefetch-buffer) ----
   Aviary-themed client prefetch buffer chrome: the utils-zone chip (nested
   count), the drawer's NEST bar (total MB · nest-ahead limit · Nest now), and
   the per-queued-track egg indicator (pending / downloading %% / nested). All
   of it is revealed by mo-player.js only when NESTING is relevant (direct R2
   srcs / a non-empty nest) — Operator/dev (local srcs) never sees it. */
.mo-nest-chip { font-size: 1.05rem; }
.mo-nest-chip .mo-nest-count { font-size: .8rem; }
.mo-nest-bar { background: var(--mo-surface-3); color: var(--mo-stage-text); }
.mo-nest-label { color: var(--mo-stage-muted); white-space: nowrap; }
#mo-player .mo-nest-limit {
  width: 4.4rem; padding: .1rem .35rem;
  background: var(--mo-surface-2); color: var(--mo-stage-text);
  border-color: var(--mo-border);
}
.mo-queue-list .mo-nest-state {
  flex: 0 0 auto; min-width: 2.4rem; text-align: right;
  font-size: .78em; font-variant-numeric: tabular-nums;
  color: var(--mo-stage-muted);
}
.mo-queue-list .mo-nest-state[data-nest-state="nested"] { color: var(--mo-accent); }
.mo-queue-list .mo-nest-state[data-nest-state="na"] { min-width: 0; }

/* NESTING egg status on ALBUM TRACK LISTS (spec-musicorgan-queue-owns-the-
   transport-and-eggs-on-track-lists). The drawer rules above are flex-item rules
   scoped to .mo-queue-list; a table cell needs its own inline treatment. A slot
   that is dormant ("na") or not yet painted (empty) must occupy NOTHING — the
   Operator surface is all-local srcs, so it gains no chrome at all. */
tr[data-track-id] .mo-nest-state {
  display: inline-flex; align-items: center; gap: .2rem;
  margin-left: .25rem; vertical-align: middle;
  font-size: .78em; font-variant-numeric: tabular-nums;
  color: var(--mo-stage-muted);
}
tr[data-track-id] .mo-nest-state[data-nest-state="nested"] { color: var(--mo-accent); }
tr[data-track-id] .mo-nest-state:empty,
tr[data-track-id] .mo-nest-state[data-nest-state="na"] { display: none; }

/* play/now-playing affordances in lists — SLICE 3 now-playing treatment
   (spec-musicorgan-playback-focus-and-nav-prominence): clear accent bar + tint +
   a small animated equaliser glyph injected by highlightPlaying(). Visual only. */
tr.mo-row-playing { background:#e6fffa !important; }
tr.mo-row-playing td:first-child { box-shadow: inset 4px 0 0 var(--mo-accent); }
tr.mo-row-playing td.fw-medium, tr.mo-row-playing .fw-medium { color: var(--mo-accent); }
.mo-eq {
  color: var(--mo-accent); margin-right:.3rem; font-size:1rem; vertical-align:-0.1em;
  animation: mo-eq-pulse 1.1s ease-in-out infinite;
}
@keyframes mo-eq-pulse { 0%,100% { opacity:.45; transform:scaleY(.78); } 50% { opacity:1; transform:scaleY(1); } }
@media (prefers-reduced-motion: reduce) { .mo-eq { animation: none; opacity:1; } }

/* SLICE 2 — sibling-album badges in the artist header (compact wrap-row of small
   owl chips; NOT full cards). TAP the chip = focus (hx-get); the play glyph on it
   = play the album (.mo-play-list) without changing focus. Keep chips dense. */
.mo-album-badge-row { display:flex; flex-wrap:wrap; gap:.4rem .5rem; align-items:center; }
.mo-album-badge-wrap { display:inline-flex; align-items:center; }
.mo-album-badge {
  display:inline-flex; align-items:center; gap:.15rem;
  padding:.2rem .5rem; border:1px solid var(--mo-border); border-radius:1rem;
  background:var(--mo-surface-2); color:var(--mo-text); font-size:.82rem; line-height:1.2;
  max-width:16rem; transition:border-color .12s ease, background .12s ease;
}
.mo-album-badge:hover, .mo-album-badge:focus-visible { border-color:var(--mo-accent); background:var(--mo-surface-3); color:var(--mo-text); }
.mo-album-badge-name { overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
.mo-album-badge-yr { color:var(--mo-text-muted); font-variant-numeric:tabular-nums; }
.mo-album-badge-play {
  display:inline-flex; align-items:center; justify-content:center;
  width:1.7rem; height:1.7rem; margin-left:-.55rem;
  border:1px solid var(--mo-border); border-left:0; border-radius:0 1rem 1rem 0;
  background:var(--mo-surface-2); color:var(--mo-accent); cursor:pointer; font-size:.9rem;
}
.mo-album-badge-play:hover, .mo-album-badge-play:focus-visible { background:var(--mo-accent); color:#fff; border-color:var(--mo-accent); }

/* SLICE 4 — inline "You might like" pinned under the focused tracklist. */
.mo-you-might-like { background:var(--mo-surface-2); }

/* inline track-detail expansion row */
tr.mo-detail-row > td { background:#f8fafc; }
.mo-facts { display:grid; grid-template-columns: repeat(auto-fill, minmax(11rem,1fr)); gap:.35rem 1.25rem; }
.mo-facts dt { font-size:.7rem; text-transform:uppercase; letter-spacing:.04em; color:#718096; margin:0; }
.mo-facts dd { margin:0 0 .35rem 0; font-size:.9rem; }
.mo-mb { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size:.72rem; }

/* star rating widget */
.mo-stars { display:inline-flex; cursor:pointer; }
.mo-stars i { color:#cbd5e0; }
.mo-stars i.on { color:#ecc94b; }

/* row-ID affordance — tiny muted chip + inline copy popover (no layout shift) */
.mo-id-chip { display:inline-flex; align-items:center; gap:.2rem; vertical-align:baseline; }
.mo-id-text { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size:.68rem; color:#a0aec0; line-height:1; white-space:nowrap; }
.mo-id-info { font-size:.72rem; color:#a0aec0 !important; line-height:1; text-decoration:none; }
.mo-id-info:hover { color:var(--mo-accent) !important; }
.mo-id-pop { display:inline-flex; align-items:center; }
.mo-id-copy { font-size:.68rem; line-height:1.1; }
.mo-id-copy.mo-id-copied { color:#22863a; border-color:#22863a; }

/* alpha jump bar */
.mo-alpha { display:flex; flex-wrap:wrap; gap:.15rem; }
.mo-alpha a { width:1.7rem; text-align:center; }
.mo-alpha a.active { background:var(--mo-accent); color:#fff; border-color:var(--mo-accent); }

/* transcode status badge on the now-playing portlet
   (spec-musicorgan-transcode-web-rendition) — appended block, do not reorder */
.mo-transcode-badge {
  margin-top:.15rem; font-size:.72rem; line-height:1.2; font-weight:600;
  display:inline-flex; align-items:center; gap:.3rem;
  padding:.1rem .45rem; border-radius:.6rem;
}
.mo-transcode-wait { color:#0c2b34; background:#7fd1de; }
.mo-transcode-fail { color:#fff; background:#b23a48; }
.mo-transcode-wait::before { content:"\F130"; font-family:"bootstrap-icons"; animation:mo-tc-pulse 1.2s ease-in-out infinite; }
.mo-transcode-fail::before { content:"\F623"; font-family:"bootstrap-icons"; }
@keyframes mo-tc-pulse { 0%,100% { opacity:.4; } 50% { opacity:1; } }

/* ============ Browse CTAs — always-on Artists/Albums/Genres/Search ===========
   spec-musicorgan-responsive-browse-cta. One partial, two placements: inline in
   the banner at >=lg, a full-width touch bar at <lg (never in the hamburger). */
.mo-aviary { vertical-align: -0.18em; }

/* ============ Genres enrichment (spec-musicorgan-genres-enrichment) =========
   Curated-icon cards on the index + an artist word-cloud on the detail page.
   All token-themed (light default, dark via [data-bs-theme="dark"]). */
.mo-genre-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(13rem, 1fr)); gap: .6rem; }
.mo-genre-card {
  display: flex; align-items: center; gap: .7rem; text-decoration: none;
  padding: .7rem .8rem; border-radius: .7rem;
  background: var(--mo-surface-2); border: 1px solid var(--mo-border); color: var(--mo-text);
  transition: border-color .12s ease, background .12s ease;
}
.mo-genre-card:hover, .mo-genre-card:focus-visible { border-color: var(--mo-accent); background: var(--mo-surface-3); }
.mo-genre-card-icon, .mo-genre-head-icon {
  flex: 0 0 auto; display: inline-flex; align-items: center; justify-content: center;
  border-radius: .55rem; color: var(--mo-accent);
  background: color-mix(in srgb, var(--mo-accent) 14%, transparent);
}
.mo-genre-card-icon { width: 2.2rem; height: 2.2rem; font-size: 1.15rem; }
.mo-genre-head-icon { width: 3rem; height: 3rem; font-size: 1.6rem; border-radius: .7rem; }
.mo-genre-card-body { display: flex; flex-direction: column; min-width: 0; }
.mo-genre-card-name { font-weight: 600; line-height: 1.2; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.mo-genre-card-counts { font-size: .78rem; color: var(--mo-text-muted); }

/* LIGHT cta buttons (spec-musicorgan-style-system): light fill + dark text +
   defined border; the role accent is a hover/focus tint, never a dark fill. */
.mo-cta {
  display: inline-flex; align-items: center; gap: .4rem;
  text-decoration: none; color: var(--mo-cta-text);
  border: 1px solid var(--mo-cta-border); border-radius: .5rem;
  background: var(--mo-cta-bg); white-space: nowrap;
  transition: background .12s ease, border-color .12s ease, color .12s ease;
}
.mo-cta:hover, .mo-cta:focus-visible {
  color: var(--mo-cta-text); border-color: var(--mo-role-accent);
  background: var(--mo-cta-hover-bg);
}
.mo-cta .mo-cta-icon { flex: 0 0 auto; }
.mo-cta i.mo-cta-icon { font-size: 1.05rem; color: var(--mo-role-accent); }

/* NAVIGATOR bar — ONE large always-on band below the banner at EVERY width
   (spec-musicorgan-playback-focus-and-nav-prominence SLICE 1). The retired
   .mo-cta-inline navbar fork is gone (the 992px bug). "More icon, less box":
   the glyph dominates and the chrome is trimmed, while the tap target stays
   >=44px. It's now a direct child of <header> (not inside .navbar), so the
   old 100vw breakout is unneeded — a plain full-width flex band suffices. */
.mo-cta-bar {
  display: flex; gap: .25rem;
  padding: .3rem .4rem;
  width: 100%; box-sizing: border-box;
  background: var(--mo-stage-bg); border-top: 1px solid var(--mo-border);  /* OPAQUE — blends into the player stage tone; list must not scroll behind the nav verbs (spec-musicorgan-now-playing-opaque-header) */
}
.mo-cta-bar .mo-cta {
  flex: 1 1 0; min-width: 0; min-height: 56px; overflow: hidden;
  flex-direction: column; gap: .1rem; padding: .25rem .2rem;
  font-size: .8rem; font-weight: 600; line-height: 1.05;
  border: 0; background: transparent; border-radius: .4rem;
  color: var(--mo-cta-text);
  transition: background .12s ease, color .12s ease;
}
/* bigger glyph, tighter chrome — the 22→28 bump wasn't enough; push to 40px. */
.mo-cta-bar .mo-cta .mo-aviary { width: 40px; height: 40px; transition: transform .12s ease; }
.mo-cta-bar .mo-cta i.mo-cta-icon { font-size: 1.9rem; }
.mo-cta-bar .mo-cta-label { line-height: 1; max-width: 100%; }
/* hover = raised chip. The band now sits on the stage tone, so the old
   --mo-cta-hover-bg (== stage tone in light) would vanish; lift to the elevated
   surface instead, which contrasts against the band in both themes. */
.mo-cta-bar .mo-cta:hover, .mo-cta-bar .mo-cta:focus-visible {
  background: var(--mo-surface-2); color: var(--mo-cta-text); border-color: transparent;
}
[data-bs-theme="dark"] .mo-cta-bar .mo-cta:hover,
[data-bs-theme="dark"] .mo-cta-bar .mo-cta:focus-visible {
  background: var(--mo-surface-3);
}
.mo-cta-bar .mo-cta:hover .mo-aviary,
.mo-cta-bar .mo-cta:focus-visible .mo-aviary { transform: translateY(-2px) scale(1.06); }
.mo-cta-bar .mo-cta:hover .mo-cta-label,
.mo-cta-bar .mo-cta:focus-visible .mo-cta-label {
  text-decoration: underline; text-underline-offset: 3px;
}
/* iPhone-SE / very narrow: trim a hair but keep glyph dominant + label readable. */
@media (max-width: 360px) {
  .mo-cta-bar { gap: .2rem; padding: .25rem .25rem; }
  .mo-cta-bar .mo-cta { min-height: 50px; padding: .2rem .1rem; font-size: .74rem; }
  .mo-cta-bar .mo-cta .mo-aviary { width: 32px; height: 32px; }
  .mo-cta-bar .mo-cta i.mo-cta-icon { font-size: 1.55rem; }
}

/* Search landing hero + on-page field
   (spec-musicorgan-player-usability-icons-search-crosslinks B). */
.mo-search-form { max-width: 38rem; }
.mo-search-hero .mo-aviary { filter: drop-shadow(0 4px 10px rgba(0,0,0,.12)); }

/* ============ Cloud-player cold-start "Charter is waking" cue ==================
   spec-musicorgan-player-cold-start-waking. While the Fly machine is cold (a
   transport click queued, awaiting first backend response) the transport wears a
   light, NON-BLOCKING waking state: a slim themed strip at the top of the player
   ("Charter is waking…") + a dimmed, busy-cursor look on the gated buttons. The
   action is queued, not dropped — this is purely the visual cue. Cleared the
   instant the backend responds (stream headers / healthz 200). Charter/lair motif
   via the moon-stars owl glyph + the Charter accent. Reduced-motion -> static. */
.mo-waking {
  display: flex; align-items: center; gap: .45rem;
  padding: .2rem .75rem; font-size: .78rem; font-weight: 600;
  color: var(--charter-eye, #fff);
  background: linear-gradient(90deg, var(--charter-shade, #2f7fd6), var(--charter-accent, #4aa3ff));
  letter-spacing: .01em;
}
.mo-waking[hidden] { display: none; }
.mo-waking-owl { display: inline-flex; }
.mo-waking-owl .bi { animation: mo-waking-rouse 1.4s ease-in-out infinite; transform-origin: center; }
@keyframes mo-waking-rouse {
  0%, 100% { opacity: .55; transform: translateY(0) rotate(-6deg); }
  50%      { opacity: 1;   transform: translateY(-1px) rotate(6deg); }
}
/* Gated transport buttons read as "rousing": dimmed + busy cursor, gentle pulse.
   Still clickable (a second click just re-queues the latest intent). */
.mo-waking-btn { opacity: .6; cursor: progress; animation: mo-waking-pulse 1.4s ease-in-out infinite; }
@keyframes mo-waking-pulse { 0%, 100% { opacity: .5; } 50% { opacity: .85; } }
@media (prefers-reduced-motion: reduce) {
  .mo-waking-owl .bi, .mo-waking-btn { animation: none !important; }
  .mo-waking-btn { opacity: .6; }   /* static dim — the label still reads "waking…" */
}

/* === spec-musicorgan-browse-grid-polish (appended; do not merge above) =======
   Defect 2: long card titles bled into the neighbor column. Bootstrap 5.3 has
   no `min-w-0` utility, so the flex child kept min-width:auto and refused to
   shrink below intrinsic content width, defeating the truncate. Define the
   utility once (benefits every flex child carrying `min-w-0`), and switch the
   card title from single-line ellipsis to a 2-line wrapping clamp (operator
   prefers wraps over ellipsis). `h-100` on the card keeps 1- and 2-line cards
   equal height in a row. */
.min-w-0 { min-width: 0 !important; }
.mo-card-title {
  min-width: 0;
  white-space: normal;
  overflow-wrap: anywhere;          /* break rare unbroken tokens before bleeding */
  /* !important so Bootstrap's d-* display utilities (display:…!important) can't
     defeat the -webkit-box clamp if the title <a> ever carries one. */
  display: -webkit-box !important;
  -webkit-line-clamp: 2;            /* wrap to 2 lines, then ellipsis */
  line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
}

/* === Shows / Sets browse — GONZO/STAINED_GLASS polish (appended; do not merge) ==
   spec-musicorgan-sets-phase3-browse-impl visual pass. /sets/ index gains real
   year groups, cover thumbnails, a source-type colour language + legend, and an
   illustrated Ravon empty state; /set/<id>/ gains a signal-chain provenance block
   + cross-link rail. All token-themed (light default + dark via data-bs-theme).
   No new data/routes — purely the SCOUT/GONZO layer over the shipped contract. */

/* --- source-type colour language (SBD/AUD/MTX/FM/UNK) ----------------------
   ONE legible palette shared by the index chips, the legend, and the show
   header, so a taper's source reads at a glance. Keyed by data-src so the
   view's Bootstrap source_class stays the rendered fallback (and the SBD/AUD
   text the verifier asserts is untouched). */
.mo-src {
  display: inline-flex; align-items: center; gap: .25rem;
  font-size: .72rem; font-weight: 700; letter-spacing: .03em;
  padding: .12rem .4rem; border-radius: .45rem; line-height: 1.1;
  border: 1px solid transparent; font-variant-numeric: tabular-nums;
}
.mo-src::before { font-family: "bootstrap-icons"; font-weight: normal; font-size: .9em; }
.mo-src[data-src="SBD"] { color:#0b3d1e; background:#bff0cf; border-color:#7bd69a; }  /* clean board feed — green */
.mo-src[data-src="SBD"]::before { content:"\F2E5"; }   /* plug — board */
.mo-src[data-src="AUD"] { color:#0a3a45; background:#bfe9f2; border-color:#7fcfe0; }  /* stealth mic — cyan */
.mo-src[data-src="AUD"]::before { content:"\F3D9"; }   /* mic */
.mo-src[data-src="MTX"] { color:#2a1b52; background:#d8cffb; border-color:#b3a3f0; }  /* matrix blend — violet */
.mo-src[data-src="MTX"]::before { content:"\F4E9"; }   /* sliders — blend */
.mo-src[data-src="FM"]  { color:#3a2c0a; background:#f7e6b0; border-color:#e6cf73; }  /* broadcast — amber */
.mo-src[data-src="FM"]::before { content:"\F2DA"; }    /* broadcast */
.mo-src[data-src="UNK"] { color:var(--mo-text-muted); background:var(--mo-surface-3); border-color:var(--mo-border); }
.mo-src[data-src="UNK"]::before { content:"\F505"; }   /* question */
[data-bs-theme="dark"] .mo-src[data-src="SBD"] { color:#bff0cf; background:rgba(123,214,154,.16); border-color:rgba(123,214,154,.4); }
[data-bs-theme="dark"] .mo-src[data-src="AUD"] { color:#bfe9f2; background:rgba(127,207,224,.16); border-color:rgba(127,207,224,.4); }
[data-bs-theme="dark"] .mo-src[data-src="MTX"] { color:#d8cffb; background:rgba(179,163,240,.18); border-color:rgba(179,163,240,.4); }
[data-bs-theme="dark"] .mo-src[data-src="FM"]  { color:#f7e6b0; background:rgba(230,207,115,.16); border-color:rgba(230,207,115,.4); }

.mo-src-legend { display:flex; flex-wrap:wrap; align-items:center; gap:.3rem .5rem; }
.mo-src-legend .mo-src { cursor:default; }

/* --- availability badge (ONLINE vs ARCHIVES) — make the duality unmistakable */
.mo-avail {
  display:inline-flex; align-items:center; gap:.3rem;
  font-size:.72rem; font-weight:700; letter-spacing:.02em;
  padding:.14rem .5rem; border-radius:1rem; line-height:1.1; white-space:nowrap;
}
.mo-avail-online { color:#0b3d1e; background:#c8f2d6; border:1px solid #7bd69a; }
.mo-avail-online .bi { color:#138a4a; }
.mo-avail-archive { color:#4a3503; background:#fbeec2; border:1px solid #e6cf73; }
.mo-avail-archive .bi { color:#9a6b00; }
[data-bs-theme="dark"] .mo-avail-online { color:#c8f2d6; background:rgba(123,214,154,.16); border-color:rgba(123,214,154,.4); }
[data-bs-theme="dark"] .mo-avail-online .bi { color:#7bd69a; }
[data-bs-theme="dark"] .mo-avail-archive { color:#fbeec2; background:rgba(230,207,115,.16); border-color:rgba(230,207,115,.4); }
[data-bs-theme="dark"] .mo-avail-archive .bi { color:#e6cf73; }

/* --- /sets/ year groups — collapsible section headers with counts + Ravon ---- */
.mo-set-group + .mo-set-group { margin-top: .9rem; }
.mo-set-group-head {
  display:flex; align-items:center; gap:.55rem; width:100%;
  padding:.45rem .7rem; cursor:pointer; text-align:left;
  background:var(--mo-surface-2); color:var(--mo-text);
  border:1px solid var(--mo-border); border-radius:.6rem;
  transition:border-color .12s ease, background .12s ease;
}
.mo-set-group-head:hover, .mo-set-group-head:focus-visible {
  border-color:var(--mo-accent); background:var(--mo-surface-3);
}
.mo-set-group-head .mo-aviary { flex:0 0 auto; }
.mo-set-group-yr { font-weight:700; font-size:1.05rem; font-variant-numeric:tabular-nums; }
.mo-set-group-meta { color:var(--mo-text-muted); font-size:.8rem; }
.mo-set-group-chev { margin-left:auto; color:var(--mo-text-muted); transition:transform .15s ease; }
.mo-set-group[open] .mo-set-group-chev { transform:rotate(90deg); }
.mo-set-group-head::-webkit-details-marker { display:none; }
.mo-set-group > summary { list-style:none; }
.mo-set-group > .card { margin-top:.4rem; }

/* --- /sets/ row thumbnail (reuse the AlbumArt cascade keyed by album_id) ----- */
.mo-set-thumb { width:38px; height:38px; flex:0 0 38px; object-fit:cover; border-radius:.35rem; background:var(--mo-surface-3); }

/* --- /sets/ mobile card reflow (<lg the table stacks into rows) -------------- */
.mo-set-card {
  display:block; padding:.6rem .7rem; text-decoration:none; color:var(--mo-text);
  border:1px solid var(--mo-border); border-radius:.6rem; background:var(--mo-surface-2);
}
.mo-set-card + .mo-set-card { margin-top:.4rem; }
.mo-set-card:hover, .mo-set-card:focus-visible { border-color:var(--mo-accent); background:var(--mo-surface-3); }
.mo-set-card-title { font-weight:600; line-height:1.2; }
.mo-set-card-meta { font-size:.8rem; color:var(--mo-text-muted); }

/* --- illustrated empty state -------------------------------------------------- */
.mo-set-empty { text-align:center; padding:2.5rem 1rem; color:var(--mo-text-muted); }
.mo-set-empty .mo-aviary { opacity:.85; filter:drop-shadow(0 6px 14px rgba(0,0,0,.14)); margin-bottom:.6rem; }

/* --- /set/<id>/ provenance signal-chain block ------------------------------- */
.mo-prov {
  border:1px solid var(--mo-border); border-radius:.6rem;
  background:var(--mo-surface-2); padding:.7rem .85rem;
}
.mo-prov-title { font-size:.7rem; text-transform:uppercase; letter-spacing:.06em; color:var(--mo-text-muted); display:flex; align-items:center; gap:.35rem; margin-bottom:.5rem; }
.mo-prov-grid { display:grid; grid-template-columns:max-content 1fr; gap:.3rem .75rem; align-items:baseline; }
.mo-prov-grid dt { font-size:.74rem; color:var(--mo-text-muted); text-transform:uppercase; letter-spacing:.03em; margin:0; }
.mo-prov-grid dd { margin:0; font-size:.9rem; }
/* the lineage rendered as a signal chain: tokens split on > with chevrons */
.mo-chain { display:flex; flex-wrap:wrap; align-items:center; gap:.25rem .15rem; }
.mo-chain-node {
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size:.78rem;
  padding:.05rem .4rem; border-radius:.35rem;
  background:var(--mo-surface-3); border:1px solid var(--mo-border); color:var(--mo-text);
}
.mo-chain-arrow { color:var(--mo-accent); font-size:.8rem; }

/* --- /set/<id>/ cross-link rail --------------------------------------------- */
.mo-xlinks { display:flex; flex-wrap:wrap; gap:.4rem; }
.mo-xlink {
  display:inline-flex; align-items:center; gap:.3rem; text-decoration:none;
  font-size:.8rem; padding:.22rem .55rem; border-radius:1rem;
  background:var(--mo-surface-2); border:1px solid var(--mo-border); color:var(--mo-text);
  transition:border-color .12s ease, background .12s ease;
}
.mo-xlink:hover, .mo-xlink:focus-visible { border-color:var(--mo-accent); background:var(--mo-surface-3); color:var(--mo-text); }
.mo-xlink .bi { color:var(--mo-accent); }

/* --- archive Request CTA — a clear call to action, not a dead button -------- */
.mo-set-request {
  display:inline-flex; align-items:center; gap:.4rem;
  font-weight:600; padding:.4rem .9rem; border-radius:.5rem;
  color:#4a3503; background:#fbeec2; border:1px solid #e6cf73; text-decoration:none;
  transition:filter .12s ease;
}
.mo-set-request:hover, .mo-set-request:focus-visible { filter:brightness(.96); color:#4a3503; }
[data-bs-theme="dark"] .mo-set-request { color:#fbeec2; background:rgba(230,207,115,.16); border-color:rgba(230,207,115,.45); }

/* ---------- CATALOG_TRIAGE: click-to-enlarge refused candidate ------------
   The lane thumb is 88px under object-fit:cover — good for scanning, useless
   for reading a sleeve. <details> gives us expand/collapse with no modal and
   no inline event handler; this styles the summary so it reads as a control
   and lets the opened panel show the full-resolution image big enough to read.
   (operator 2026-08-19: "make it possible to click the cover art suggestion
   and have it expand to a large zoomable image so we can read the text on it") */
.mo-cand-zoom > summary.mo-cand-summary {
  position: relative;
  cursor: zoom-in;
  list-style: none;
  display: inline-block;
  line-height: 0;              /* no baseline gap under the thumb */
}
.mo-cand-zoom > summary.mo-cand-summary::-webkit-details-marker { display: none; }
.mo-cand-zoom[open] > summary.mo-cand-summary { cursor: zoom-out; }
.mo-cand-hint {
  position: absolute;
  right: 2px; bottom: 2px;
  background: rgba(0, 0, 0, .62);
  color: #fff;
  border-radius: .2rem;
  font-size: .68rem;
  line-height: 1;
  padding: .15rem .25rem;
}
.mo-cand-zoom[open] > summary .mo-cand-hint { opacity: .35; }
/* The opened panel escapes the 88px flex column so the art gets real width.
   max-height + auto overflow means a taller-than-viewport sleeve PANS rather
   than pushing the whole lane down the page. */
.mo-cand-full {
  overflow: auto;
  max-height: 78vh;
  max-width: min(92vw, 620px);
}
.mo-cand-full img {
  width: 100%;
  height: auto;
  display: block;
  background: #eee;
}

/* ================================================ login wall — onboarding v2 (S1)
   spec-musicorgan-cloud-player-onboarding-invite-wall-htmx-welcome-email-tab-reuse
   pattern-htmx-smooth-transitions-swap-settle-focus-reduced-motion.
   Scoped to the standalone login.html; none of this reaches the app shell. */

/* Rule 3 — HOLD THE LAYOUT. The panel reserves its height so the card below it
   (the song-nod footer) does not jump when the form becomes a message. Sized to
   the tallest state, the why-copy + form. */
.mo-invite-panel { min-height: 21rem; }

/* Rule 2 — FADE THROUGH SWAP AND SETTLE, 150ms each way (under the 200ms
   ceiling; longer reads as lag). htmx puts .htmx-swapping on the OUTGOING node
   and .htmx-settling on the INCOMING one, so the incoming node must start at
   opacity 0 and fade UP to the base rule as the class is removed — settling
   pinned at 1 would be a no-op fade-in. */
.mo-invite-panel { opacity: 1; transition: opacity 150ms ease-in; }
.mo-invite-panel.htmx-swapping { opacity: 0; transition: opacity 150ms ease-out; }
.mo-invite-panel.htmx-settling { opacity: 0; }

/* Rule 3 — the busy state shows IN PLACE on the button, never as a page-level
   bar. htmx marks the requesting form .htmx-request for the life of the call. */
.mo-invite-busy-label { display: none; }
.mo-invite-panel form.htmx-request .mo-invite-idle-label { display: none; }
.mo-invite-panel form.htmx-request .mo-invite-busy-label {
  display: inline-flex;
  align-items: center;
  gap: .4rem;
}
.mo-invite-panel form.htmx-request button[type="submit"] {
  pointer-events: none;
  opacity: .75;
}

/* Rule 4 — the one-shot highlight that carries the eye from the sent card to the
   passphrase field. mo-invite-panel.js adds .mo-flash and removes it on
   animationend, so the next add genuinely restarts the animation. */
@keyframes mo-flash {
  0%   { box-shadow: 0 0 0 .28rem rgba(44, 122, 123, .45); }
  100% { box-shadow: 0 0 0 .28rem rgba(44, 122, 123, 0); }
}
.mo-flash { animation: mo-flash 1.2s ease-out 1; }

/* The destination card. Bootstrap utilities carry the layout; only the icon
   fallback tint needs a rule of its own. */
.mo-destination-icon { color: #2c7a7b; }

/* Rule 6 — RESPECT REDUCED MOTION. The reduced path is an INSTANT swap, not a
   broken one: opacity is pinned at 1 in every phase so content is never left
   invisible, and the highlight simply does not animate. */
@media (prefers-reduced-motion: reduce) {
  .mo-invite-panel,
  .mo-invite-panel.htmx-swapping,
  .mo-invite-panel.htmx-settling { transition: none; opacity: 1; }
  .mo-flash { animation: none; }
}

/* ============================================================= rarity (S2/S3)
   spec-musicorgan-streamer-presence-chips-and-rarity-tiers-on-album-surfaces.
   Chips + tier chip + Check-now + override form + grid-card dot + track line.
   OPERATOR-only surfaces (mo_extras.rarity_* filters + the album/card/panel
   templates all gate on `not read_only`); never seen on the Cloud Player. */

/* --- grid-card / accordion-panel corner dot — quiet: unchecked paints nothing */
.mo-rarity-corner { position: absolute; top: .35rem; right: .35rem; z-index: 2; }
.mo-rarity-dot {
  display: inline-block; width: .55rem; height: .55rem; border-radius: 50%;
  box-shadow: 0 0 0 2px var(--mo-surface-2);
}
.mo-rarity-dot-everywhere { background: var(--mo-text-muted); }
.mo-rarity-dot-tidal-only { background: #0dcaf0; }
.mo-rarity-dot-rare,
.mo-rarity-dot-operator-rare { background: #ffc107; }

/* --- album-page rarity panel (chips + tier + Check-now + override form) --- */
.mo-rarity-panel { min-height: 2rem; }
.mo-rarity-svc { text-decoration: none; }
.mo-rarity-check-form,
.mo-rarity-override-form { display: inline-flex; align-items: center; gap: .3rem; }
.mo-rarity-override-form select,
.mo-rarity-override-form input[type="text"] { font-size: .8rem; }
.mo-rarity-override-form select { max-width: 9rem; }
.mo-rarity-override-form input[type="text"] { max-width: 12rem; }

/* Smooth swap/settle for the Check-now round-trip (pattern-htmx-smooth-
   transitions-swap-settle-focus-reduced-motion, rule 2 — CORRECTED polarity:
   .htmx-swapping fades the OUTGOING node out; .htmx-settling holds the
   INCOMING node invisible so it rises into the base rule's fade-in as the
   class is removed). Worked reference: .mo-invite-panel above. */
.mo-rarity-panel { opacity: 1; transition: opacity 150ms ease-in; }
.mo-rarity-panel.htmx-swapping { opacity: 0; transition: opacity 150ms ease-out; }
.mo-rarity-panel.htmx-settling { opacity: 0; }

@media (prefers-reduced-motion: reduce) {
  .mo-rarity-panel,
  .mo-rarity-panel.htmx-swapping,
  .mo-rarity-panel.htmx-settling { transition: none; opacity: 1; }
}

/* --- Flare affordance (S4) — the tier-driven Flare pre-state -----------------
   One region (#mo-flare-<id>), four states: the plain Flare button, the
   two-step confirm for an album the streamers already carry, 'Check streamers
   first' when nothing has been checked, and the 'On Cloud Player' badge. The
   states differ in WIDTH, so the region reserves a min-height and the header
   row does not reflow vertically as it swaps between them (transitions rule 3:
   hold the layout). Operator-only — never rendered on the Cloud Player. */
.mo-flare-affordance { min-height: 2rem; }
.mo-flare-check-first { display: inline-flex; align-items: center; }
.mo-flare-confirm-note { max-width: 22rem; line-height: 1.2; }
.mo-flare-cancel { text-decoration: underline; }

/* Same swap/settle polarity as the rarity panel above — this region is swapped
   by its own confirm/cancel GETs AND out of band by the Check-now poll and the
   override POST, so it needs the fade in every direction. */
.mo-flare-affordance { opacity: 1; transition: opacity 150ms ease-in; }
.mo-flare-affordance.htmx-swapping { opacity: 0; transition: opacity 150ms ease-out; }
.mo-flare-affordance.htmx-settling { opacity: 0; }

@media (prefers-reduced-motion: reduce) {
  .mo-flare-affordance,
  .mo-flare-affordance.htmx-swapping,
  .mo-flare-affordance.htmx-settling { transition: none; opacity: 1; }
}
