/* Shared design system - used by the storefront AND the admin panel so a
   theme change (accent color, logo) in Admin > Settings > Branding applies
   everywhere.

   The real accent is written into every page's <head> by the server before it
   is sent (middleware/brandBoot), so the first paint is already the store's
   own color. The value below is only a last-resort fallback for a page served
   without that injection. It is deliberately NEUTRAL: it used to be the demo
   store's red, which meant any page that painted before the branding was
   applied flashed a red theme that belonged to no real shop. A grey looks
   like "not yet colored" instead of like someone else's brand. */
@import url('https://fonts.googleapis.com/css2?family=Archivo:ital,wght@0,400;0,600;0,800;1,700;1,800;1,900&family=Inter:wght@400;500;600&family=IBM+Plex+Mono:wght@400;500;600&display=swap');

:root{
  --bg:#050505;
  --bg-2:#0d0d0d;
  --panel:#111111;
  --panel-2:#171717;
  --line:rgba(255,255,255,0.08);
  --line-strong:rgba(255,255,255,0.16);
  --text:#F5F3EF;
  --text-dim:rgba(245,243,239,0.64);
  --text-faint:rgba(245,243,239,0.38);
  /* Neutral fallback only - see the note at the top of this file. Never put a
     brand color here: whatever is here is what a mis-served page flashes. */
  --accent:#6E6E6E;
  /* Readable text/icon color placed ON TOP of an accent-filled surface.
     Recomputed from the accent's luminance at runtime (server-side by
     brandBoot, client-side by brand.js) — a pale accent (white/yellow) gets
     dark ink, a dark one (blue) gets light ink. The value here matches the
     neutral fallback accent above.  */
  --accent-ink:#F5F3EF;
  --success:#4CAF6D;
  --success-ink:#0a0a0a;
  --danger:#E5484D;
  --radius:10px;
  --radius-lg:16px;
  /* One height for every control that shares a line with another one — the
     Browse strip's search input, its Filters button and its genre pills all
     read as a single row only if they are literally the same height. */
  --control-h:38px;
  --font-head:'Archivo', 'Inter', sans-serif;
  --font-body:'Inter', sans-serif;
  --font-mono:'IBM Plex Mono', monospace;
}

/* Derived shades computed from whatever --accent is set to at runtime, so
   every preset (red/yellow/white/green/blue/gold) or a fully custom color
   automatically gets a matching hover/dim/soft variant with no extra CSS. */
:root{
  --accent-dim: color-mix(in srgb, var(--accent) 55%, transparent);
  --accent-soft: color-mix(in srgb, var(--accent) 16%, transparent);
  --accent-hover: color-mix(in srgb, var(--accent) 88%, white 12%);
  /* Used wherever a track/kit has no artwork uploaded yet — a themed
     placeholder instead of a flat panel color. */
  --art-fallback: linear-gradient(135deg, color-mix(in srgb, var(--accent) 30%, var(--panel-2)), var(--bg));

  /* ---- public theme aliases ----
     The single source of truth for "the color the admin picked" and "text
     drawn in that color". Components reference these instead of repeating
     an accent value of their own, so one setting change propagates
     everywhere (nav, links, active states, headings, icons, cards).
     --theme-text-color is nudged toward white so accent-colored copy stays
     legible on the dark background even with a very dark accent. */
  --theme-color: var(--accent);
  --theme-text-color: color-mix(in srgb, var(--accent) 82%, var(--text) 18%);
  --theme-on-color: var(--accent-ink);
}

/* Optional, from Admin > Settings > Branding ("Heading color"): tints every
   heading with the store's color. Body copy keeps the neutral --text on
   purpose — tinting paragraphs is what makes a page unreadable. */
body.accent-headings h1,
body.accent-headings h2,
body.accent-headings h3,
body.accent-headings .section-heading{color:var(--theme-text-color);}

*{box-sizing:border-box;}
/* The page never moves sideways. On a phone a document even two pixels wider
   than the screen becomes draggable, and dragging it takes the background and
   the sticky header with it — the whole shop appears to come loose.
   `clip` rather than `hidden`: hidden would turn the root into a scroll
   container and break position:sticky on the header (admin.css already made
   this same call for its sidebar). Safari 15 and older ignore `clip`, so the
   `hidden` line above it is their fallback — harmless there because overflow
   set on <html> propagates to the viewport instead of creating a container.
   overscroll-behavior-x stops the sideways rubber-band and the browser's own
   swipe gesture from chaining off a page that has nothing to scroll. */
html{
  background:var(--bg);
  overflow-x:hidden;
  overflow-x:clip;
  overscroll-behavior-x:contain;
}
body{
  margin:0; background:var(--bg); color:var(--text);
  max-width:100%; overflow-x:clip;
  font-family:var(--font-body); line-height:1.6; position:relative;
  /* A long unbroken word (a pasted URL, a very long beat title) wraps
     instead of pushing the whole page sideways. Nothing is ever hidden to
     "fix" an overflow — the content reflows. */
  overflow-wrap:break-word;
}
/* Flex/grid children default to min-width:auto, which is what makes a long
   title or a wide table stretch its parent past the viewport. */
.header-inner > *, .footer-inner > *, .card-head > *{min-width:0;}
/* Anything genuinely wider than the screen (tables, code) scrolls inside
   its own container rather than dragging the page with it. */
.table-scroll{overflow-x:auto; -webkit-overflow-scrolling:touch;}
h1,h2,h3{font-family:var(--font-head); font-style:italic; font-weight:800; margin:0; letter-spacing:-0.01em;}
.mono{font-family:var(--font-mono);}
a{color:inherit; text-decoration:none;}
button{font-family:var(--font-body); cursor:pointer;}
:focus-visible{outline:2px solid var(--accent); outline-offset:2px;}
img{max-width:100%; display:block;}

/* ---------- grain + glow background, applied once per page via <body class="grained"> ---------- */
body.grained::before, body.grained::after{
  content:""; position:fixed; inset:0; pointer-events:none; z-index:0;
}
body.grained::before{
  background:
    radial-gradient(600px circle at 15% 10%, color-mix(in srgb, var(--accent) 14%, transparent), transparent 60%),
    radial-gradient(700px circle at 85% 25%, rgba(255,255,255,0.05), transparent 60%),
    radial-gradient(800px circle at 50% 90%, color-mix(in srgb, var(--accent) 10%, transparent), transparent 65%);
}
body.grained::after{
  opacity:0.05; mix-blend-mode:overlay;
  background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='120' height='120'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='2' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E");
}
/* :where() keeps this at zero specificity so any page-level rule (e.g. a
   fixed-position mini-player, cart widget, or overlay) can override
   position/z-index without a fight — a plain "body.grained > *" selector
   was winning against .mini-player{position:fixed} etc. and silently
   forcing every fixed element back into normal document flow. */
:where(body.grained) > *{position:relative; z-index:1;}

/* ---------- logo ---------- */
:root{ --logo-radius:0; }
.logo-mark{display:inline-flex; width:26px; height:26px; color:var(--theme-color); flex-shrink:0; border-radius:var(--logo-radius); overflow:hidden;}
.logo-mark svg, .logo-mark img{width:100%; height:100%; object-fit:contain;}
.logo-mark img{object-fit:cover;}
/* No logo uploaded: the slot collapses instead of leaving a 26px hole beside
   the word mark (brand.js sets this class when settings.brand_logo is empty). */
.logo-mark.is-empty{display:none;}
/* The store name is half of the logo — the mark beside it is already
   --theme-color, so leaving the word at the default body color meant the
   accent picked in Admin > Settings > Branding visibly stopped halfway
   through the logo. --theme-text-color rather than raw --accent so a dark
   accent stays legible on the dark ground (see the tokens at the top). */
.logo-word{font-family:var(--font-head); font-style:italic; font-weight:800; font-size:17px; letter-spacing:0.01em; color:var(--theme-text-color);}

/* ---------- top nav with "/" separators, mono uppercase ---------- */
.topnav{display:flex; align-items:center; gap:10px; font-family:var(--font-mono); font-size:12px; text-transform:uppercase; letter-spacing:0.06em; flex-wrap:wrap;}
.topnav a{color:var(--text-dim); padding:4px 2px;}
.topnav a:hover, .topnav a.active{color:var(--theme-text-color);}
.topnav .sep{color:var(--line-strong);}

/* ============================================================================
   Global site header — markup injected by header.js, styled once here so
   every page's bar is identical (logo, nav, search, account, cart, and the
   mobile dropdown). Pages must not restate these rules.
   ========================================================================== */
.site-header{position:sticky; top:0; z-index:40; background:rgba(5,5,5,0.85); backdrop-filter:blur(8px); border-bottom:1px solid var(--line); display:block;}
/* A flat flex row of blocks in the admin's configured order. The old
   centering used position:absolute, which let the logo sit on top of the
   nav; every block is a normal flex item now, so overlap is impossible. */
.header-inner{position:relative; display:flex; align-items:center; gap:16px; padding:14px 24px; max-width:1200px; margin:0 auto;}
.hb-block{flex-shrink:0; min-width:0;}
/* eats the free space — this is what pushes the actions to the right, and
   two of them are what actually centres the logo */
.hb-spacer{flex:1 1 0%; min-width:0;}
.logo{display:flex; align-items:center; gap:10px; min-width:0;}
.hb-logo{display:flex;}
.header-search-slot:empty{display:none;}

.search-box{display:flex; align-items:center; gap:8px; background:var(--panel-2); border:1px solid var(--line-strong); border-radius:999px; padding:7px 14px; width:clamp(150px, 18vw, 240px); transition:border-color 150ms;}
.search-box:focus-within{border-color:var(--theme-color);}
.search-box .search-icon{font-size:12px; color:var(--text-faint); flex-shrink:0;}
.search-box input{background:transparent; border:none; padding:0; font-size:12px; font-family:var(--font-mono); width:100%; min-width:0;}
.search-box input:focus{outline:none;}
.search-box input::-webkit-search-cancel-button{filter:invert(0.6);}

.account-link{font-size:12px; font-family:var(--font-mono); text-transform:uppercase; letter-spacing:0.05em; color:var(--text-dim); border:1px solid var(--line-strong); border-radius:999px; padding:8px 16px; white-space:nowrap;}
.account-link:hover{border-color:var(--theme-color); color:var(--theme-text-color);}
.header-cart-btn{position:relative; background:transparent; border:1px solid var(--line-strong); color:var(--text); border-radius:999px; width:38px; height:38px; display:flex; align-items:center; justify-content:center; flex-shrink:0;}
.header-cart-btn:hover{border-color:var(--theme-color); color:var(--theme-text-color);}
.header-cart-btn svg{width:17px; height:17px;}
.header-cart-btn .cart-count{position:absolute; top:-6px; right:-6px; background:var(--theme-color); color:var(--theme-on-color); font-size:10px; font-weight:700; border-radius:50%; width:16px; height:16px; display:flex; align-items:center; justify-content:center;}

/* hamburger — hidden on desktop, shown once the bar can no longer fit */
.nav-toggle{display:none; flex-direction:column; justify-content:center; gap:4px; width:38px; height:38px; margin-left:auto; background:transparent; border:1px solid var(--line-strong); border-radius:999px; flex-shrink:0;}
.nav-toggle span{display:block; width:16px; height:1.5px; background:var(--text); margin:0 auto; transition:transform 160ms, opacity 160ms;}
.nav-toggle[aria-expanded="true"] span:nth-child(1){transform:translateY(5.5px) rotate(45deg);}
.nav-toggle[aria-expanded="true"] span:nth-child(2){opacity:0;}
.nav-toggle[aria-expanded="true"] span:nth-child(3){transform:translateY(-5.5px) rotate(-45deg);}

@media (max-width:900px){
  /* Below the breakpoint the configured order stops mattering: the logo
     stays on the bar and everything else drops into a full-width panel, so
     no arrangement can push a block off-screen at 375px. */
  .header-inner{gap:12px; padding:12px 16px; flex-wrap:nowrap;}
  .nav-toggle{display:flex; order:99; margin-left:auto;}
  .header-inner .hb-logo{order:-1;}
  .header-inner .hb-spacer{display:none;}
  .header-inner > .hb-block:not(.hb-logo){display:none;}
  .header-inner.menu-open > .hb-block:not(.hb-logo){display:flex;}
  .header-inner.menu-open{
    flex-wrap:wrap; align-items:stretch; row-gap:12px;
    padding-bottom:16px;
  }
  .header-inner.menu-open > .hb-block:not(.hb-logo){flex:1 1 100%;}
  .header-inner.menu-open .topnav{flex-wrap:wrap; gap:14px; font-size:13px;}
  .header-inner.menu-open .topnav .sep{display:none;}
  .header-inner.menu-open .search-box{width:100%;}
  .header-inner.menu-open .account-link{text-align:center;}
  .header-inner.menu-open .header-cart-btn{flex:0 0 38px;}
  .header-inner.menu-open .hb-custom{flex:0 1 auto;}
}

/* ============================================================================
   Head bar — logo size + custom blocks (Admin > Settings > Head bar).
   The "free space" of the bar is a real, fillable zone now: an admin can drop
   a promo line, a call-to-action, a link, a small image or an icon into it,
   each with its own per-breakpoint visibility so a rich desktop bar can still
   stay clean on a phone. Markup comes from header.js (customBlockMarkup).
   ========================================================================== */
.header-inner[data-logo-size="small"] .logo-mark{width:20px; height:20px;}
.header-inner[data-logo-size="small"] .logo-word{font-size:15px;}
.header-inner[data-logo-size="large"] .logo-mark{width:34px; height:34px;}
.header-inner[data-logo-size="large"] .logo-word{font-size:21px;}

.hb-custom{display:inline-flex; align-items:center; min-width:0;}
.hb-custom-text{
  font-family:var(--font-mono); font-size:11px; text-transform:uppercase;
  letter-spacing:0.06em; color:var(--text-dim); white-space:nowrap;
  overflow:hidden; text-overflow:ellipsis;
}
.hb-cta{
  background:var(--theme-color); color:var(--theme-on-color); border-radius:999px;
  padding:8px 16px; font-size:12px; font-family:var(--font-mono);
  text-transform:uppercase; letter-spacing:0.05em; white-space:nowrap;
  transition:opacity 150ms, transform 120ms;
}
.hb-cta:hover{opacity:0.88; color:var(--theme-on-color);}
.hb-cta:active{transform:scale(0.96);}
.hb-textlink{
  font-size:12px; font-family:var(--font-mono); text-transform:uppercase;
  letter-spacing:0.05em; color:var(--text-dim); white-space:nowrap;
}
.hb-textlink:hover{color:var(--theme-text-color);}
.hb-custom-image img{height:26px; width:auto; max-width:130px; object-fit:contain; display:block;}
.hb-iconlink{
  width:34px; height:34px; border-radius:999px; justify-content:center;
  border:1px solid var(--line-strong); color:var(--text-dim); flex-shrink:0;
  transition:border-color 150ms, color 150ms;
}
.hb-iconlink svg{width:16px; height:16px;}
a.hb-iconlink:hover{border-color:var(--theme-color); color:var(--theme-text-color);}

/* Per-block responsive visibility. The breakpoints match the ones the admin
   picker offers: mobile ≤640, tablet 641–1024, desktop >1024. */
@media (min-width:1025px){ .hb-hide-desktop{display:none !important;} }
@media (min-width:641px) and (max-width:1024px){ .hb-hide-tablet{display:none !important;} }
@media (max-width:640px){ .hb-hide-mobile{display:none !important;} }

/* ============================================================================
   Global site footer — the markup comes from footer.js, these are its only
   styles. Every page shares this one block now (it used to be copy-pasted,
   and drifted, in index.html / kits.html / kit.html / page.html), so the
   footer looks identical on the storefront, the beat and kit pages, the
   editorial pages, the checkout/404 screens and the whole /account section.
   ========================================================================== */
.site-footer{padding:40px 24px 30px; border-top:1px solid var(--line); margin-top:24px;}
.footer-inner{max-width:1200px; margin:0 auto; display:flex; justify-content:space-between; flex-wrap:wrap; gap:16px; align-items:flex-end;}
/* The oversized brand word is the footer's signature. It is painted with the
   accent the admin picked in Settings > Branding (--theme-text-color is that
   accent nudged toward the text color so a very dark one stays readable) —
   the same variable the header's small logo word uses, so the two always
   match whatever preset or custom color is active. */
.site-footer .logo-word{font-size:clamp(30px, 5vw, 56px); line-height:0.9; letter-spacing:-0.02em; color:var(--theme-text-color);}
/* The mark is the header's job; down here the word carries the brand alone. */
.site-footer .logo .logo-mark{display:none;}
.footer-social{display:flex; gap:10px;}
.social-icon{width:30px; height:30px; border-radius:50%; border:1px solid var(--line-strong); display:flex; align-items:center; justify-content:center; color:var(--text-dim); transition:border-color 150ms, color 150ms, transform 150ms;}
.social-icon svg{width:14px; height:14px;}
.social-icon:hover{border-color:var(--theme-color); color:var(--theme-text-color); transform:translateY(-2px);}
.legal{max-width:1200px; margin:10px auto 0; font-size:11px; color:var(--text-faint);}
@media (max-width:640px){
  .site-footer{padding:24px 16px;}
}

/* Short pages — the checkout confirmation, 404, and the /account sign-in
   screens — used to center a single card in the viewport with
   `body{display:flex; align-items:center}`, which leaves no place to put a
   footer. They lay out as a column instead: .page-main takes the free space
   and centers the card inside it, and the footer is pushed to the bottom
   edge rather than floating under a half-empty screen. */
/* align-items/justify-content are reset explicitly: .auth-body (account.css)
   still centers its single card, and a stray `align-items:center` here would
   shrink the footer to the width of its own text. */
body.page-column{min-height:100vh; display:flex; flex-direction:column; align-items:stretch; justify-content:flex-start; padding:0;}
body.page-column > .site-footer{margin-top:auto;}
.page-main{flex:1 0 auto; display:flex; align-items:center; justify-content:center; width:100%; padding:32px 24px;}

/* ---------- buttons ---------- */
.btn-primary{background:var(--accent); color:var(--accent-ink); border:none; border-radius:var(--radius); padding:12px 20px; font-size:14px; font-weight:600; transition:background 150ms, transform 150ms;}
.btn-primary:hover{background:var(--accent-hover);}
.btn-primary:active{transform:scale(0.98);}
.btn-primary:disabled{opacity:0.5; cursor:not-allowed;}
.btn-ghost{background:transparent; border:1px solid var(--line-strong); color:var(--text); border-radius:var(--radius); padding:11px 18px; font-size:13px; transition:border-color 150ms, color 150ms;}
.btn-ghost:hover{border-color:var(--accent); color:var(--accent);}
.btn-ghost:disabled{opacity:0.5; cursor:not-allowed;}
.btn-danger{background:transparent; border:1px solid rgba(229,72,77,0.5); color:var(--danger); border-radius:var(--radius); padding:7px 12px; font-size:12px;}

/* Transport-style action button (Play all, …). Filled with the theme color
   and printed in --accent-ink, which brand.js derives from that color's
   luminance — so it can never end up light-on-light (or dark-on-dark)
   whichever accent the admin picks. Every state is defined explicitly. */
.btn-play{
  display:inline-flex; align-items:center; gap:8px;
  background:var(--theme-color); color:var(--theme-on-color); border:1px solid transparent;
  border-radius:999px; padding:8px 16px; font-size:12px; font-family:var(--font-mono);
  text-transform:uppercase; letter-spacing:0.05em; font-weight:600;
  transition:background 150ms, color 150ms, transform 120ms, box-shadow 150ms;
}
.btn-play svg{width:14px; height:14px; fill:currentColor;}
.btn-play:hover{background:var(--accent-hover); color:var(--theme-on-color);}
.btn-play:active{transform:scale(0.96);}
.btn-play:focus-visible{outline:2px solid var(--text); outline-offset:2px;}
.btn-play:disabled{opacity:0.45; cursor:not-allowed; transform:none;}

/* ---------- pill filters (genre/style/sort) ---------- */
.pill-row{display:flex; gap:8px; flex-wrap:wrap;}
.pill{background:var(--panel-2); border:1px solid transparent; color:var(--text-dim); border-radius:999px; padding:8px 16px; font-size:12px; font-family:var(--font-mono); text-transform:uppercase; letter-spacing:0.04em; transition:background 150ms, color 150ms, transform 100ms;}
.pill:active{transform:scale(0.94);}
.btn-ghost:active{transform:scale(0.97);}
.pill:hover{color:var(--text);}
.pill.active{background:var(--accent); color:var(--accent-ink);}

/* ---------- BrowseFilters (browsefilters.js) ----------
   The one filter bar of the storefront — Browse's pills, reused as-is by the
   homepage Kits row, the /kits page and the /tracks catalog. The pills
   themselves are the shared .pill component above; only the layout of the
   rows lives here. */
/* The gap under the bar is the only thing separating three rows of pills
   from a grid of cards, and at 16px the two read as one block — the filters
   looked crammed against the results. Scaled to the viewport so a wide page
   gets a real break without a phone losing a card below the fold. */
.browse-filters{display:flex; flex-direction:column; gap:10px; margin-bottom:clamp(20px, 3.2vw, 40px); flex:1; min-width:0;}
/* Each row is label + pills on one line, so "Sort / Newest" sits exactly on
   the same baseline as "Genre / …" instead of drifting to its own corner. */
.bf-group{display:flex; align-items:center; gap:10px; flex-wrap:wrap; min-width:0;}
.bf-label{font-family:var(--font-mono); font-size:10px; text-transform:uppercase; letter-spacing:0.08em; color:var(--text-faint); flex:0 0 46px;}
.browse-filters .pill-row{gap:8px; flex:1; min-width:0;}
/* "Show more" reads as a control, not as another filter value */
.pill.pill-more{background:transparent; border:1px dashed var(--line-strong); color:var(--text-faint);}
.pill.pill-more:hover{border-color:var(--theme-color); color:var(--theme-text-color);}
@media (max-width:640px){
  /* Pills stay tappable and wrap instead of scrolling out of reach. */
  .browse-filters{gap:8px;}
  .browse-filters .pill{padding:7px 13px; font-size:11px;}
  /* the label moves above its row rather than squeezing the pills */
  .bf-group{flex-direction:column; align-items:flex-start; gap:5px;}
  .bf-label{flex:none;}
}

/* ============================================================================
   Compact filter bar (BrowseFilters compact:true)

   The stacked version costs ~200px above the fold and used to be printed
   twice on the homepage - a visitor met four rows of pills before a single
   beat. Genre stays on the bar because it is the filter people actually
   reach for; style, price and sort fold into a panel that starts closed.
   The button keeps a dot while anything inside it is set, so a narrowed
   catalog never looks like an empty one.
   ========================================================================== */
.browse-filters.bf-compact{gap:0; margin-bottom:0;}
/* Everything on the bar hangs from its TOP edge, not from its middle: a genre
   row that wraps to a second line used to re-centre the Filters button and the
   count against a taller row, so adding one pill visibly nudged its neighbours.
   Anchored at flex-start, extra lines only ever grow downwards. */
.bf-bar{display:flex; align-items:flex-start; gap:10px; flex-wrap:wrap; min-width:0; min-height:var(--control-h);}
.bf-toggle{
  display:inline-flex; align-items:center; gap:7px; flex-shrink:0; align-self:flex-start;
  background:transparent; border:1px solid var(--line-strong); color:var(--text-dim);
  border-radius:999px; height:var(--control-h); padding:0 14px;
  font-family:var(--font-mono); font-size:11px; text-transform:uppercase; letter-spacing:0.05em;
  transition:border-color 150ms, color 150ms;
}
.bf-toggle svg{width:13px; height:13px;}
.bf-toggle:hover{border-color:var(--theme-color); color:var(--theme-text-color);}
.bf-toggle.open{border-color:var(--theme-color); color:var(--theme-text-color);}
/* the dot is the whole promise that nothing is silently hidden */
.bf-toggle.has-active{position:relative; color:var(--text); border-color:var(--theme-color);}
.bf-toggle.has-active::after{
  content:""; position:absolute; top:-3px; right:-3px;
  width:8px; height:8px; border-radius:50%;
  background:var(--theme-color); border:2px solid var(--bg);
}
/* Centred against the first line of the bar rather than against all of it. */
.bf-divider{
  width:1px; height:20px; background:var(--line-strong); flex-shrink:0;
  align-self:flex-start; margin-top:calc((var(--control-h) - 20px) / 2);
}
.bf-compact .pill-row{display:flex; flex-wrap:wrap; gap:8px; row-gap:8px; align-content:flex-start; flex:1; min-width:0;}
/* Same height as the input beside it, so pill text and search text sit on one
   baseline instead of a few pixels apart. */
.bf-compact .pill{display:inline-flex; align-items:center; height:var(--control-h); padding:0 16px;}
.bf-count{
  font-family:var(--font-mono); font-size:11px; color:var(--text-faint);
  margin-left:auto; flex-shrink:0; white-space:nowrap;
  display:inline-flex; align-items:center; align-self:flex-start; height:var(--control-h);
}
/* ---------- BPM range (BrowseFilters showBpm:true) ----------
   The only filter that is a span, so it is a two-handle slider rather than a
   fourth pill row. Both <input type=range> sit stacked on the same rail with
   transparent tracks; only their thumbs take pointer events, which is what
   lets two native inputs behave as one control. The readout under the rail
   states the span in words so nobody has to read a value off a handle. */
.bf-range{position:relative; flex:1; min-width:180px; max-width:340px; height:var(--control-h); display:flex; align-items:center;}
.bf-range-rail{position:absolute; left:0; right:0; top:50%; transform:translateY(-50%); height:3px; border-radius:999px; background:var(--line-strong);}
.bf-range-fill{position:absolute; top:0; bottom:0; border-radius:999px; background:var(--theme-color, var(--accent));}
.bf-range-input{
  position:absolute; left:0; top:50%; transform:translateY(-50%);
  width:100%; margin:0; height:var(--control-h);
  background:transparent; -webkit-appearance:none; appearance:none;
  pointer-events:none; /* the rail is decoration; only the thumbs are grabbable */
}
.bf-range-input:focus{outline:none;}
.bf-range-input::-webkit-slider-thumb{
  -webkit-appearance:none; appearance:none; pointer-events:auto;
  width:15px; height:15px; border-radius:50%; cursor:grab;
  background:var(--text); border:2px solid var(--bg);
  box-shadow:0 1px 4px rgba(0,0,0,0.5);
}
.bf-range-input::-moz-range-thumb{
  pointer-events:auto; width:15px; height:15px; border-radius:50%; cursor:grab;
  background:var(--text); border:2px solid var(--bg);
  box-shadow:0 1px 4px rgba(0,0,0,0.5);
}
.bf-range-input:focus-visible::-webkit-slider-thumb{border-color:var(--theme-color, var(--accent));}
.bf-range-input:focus-visible::-moz-range-thumb{border-color:var(--theme-color, var(--accent));}
.bf-range-input::-moz-range-track{background:transparent; border:none;}
.bf-range-out{
  position:absolute; left:0; top:100%; margin-top:-2px;
  font-family:var(--font-mono); font-size:10px; letter-spacing:0.06em;
  text-transform:uppercase; color:var(--text-faint); white-space:nowrap;
}
/* the readout hangs below the rail, so this row needs the space for it */
.bf-group.bf-bpm{padding-bottom:12px;}
@media (max-width:640px){
  .bf-range{max-width:none; width:100%;}
}

.bf-panel{
  display:flex; flex-direction:column; gap:10px;
  margin-top:14px; padding-top:14px; border-top:1px solid var(--line);
  animation:bf-open 200ms ease;
}
.bf-panel[hidden]{display:none;}
@keyframes bf-open{from{opacity:0; transform:translateY(-4px);} to{opacity:1; transform:translateY(0);}}
@media (prefers-reduced-motion:reduce){ .bf-panel{animation:none;} }
@media (max-width:640px){
  /* the genre pills scroll sideways rather than pushing the count off-screen */
  .bf-bar{flex-wrap:nowrap; overflow-x:auto; scrollbar-width:none; padding-bottom:2px;}
  .bf-bar::-webkit-scrollbar{display:none;}
  .bf-compact .pill-row{flex-wrap:nowrap;}
  .bf-compact .pill{white-space:nowrap; padding:0 13px; font-size:11px;}
  .bf-count{display:none;}
  .controls-row, .browse-filters.bf-compact{--control-h:34px;}
}

/* ---------- the Browse controls strip (search + compact filter bar) ----------
   Two columns of one grid, both anchored to the top. The search box is the
   control a visitor aims at, so it must not move: opening the Filters panel,
   wrapping the genre row onto a second line or dropping the count in all grow
   the right-hand column DOWNWARDS and leave the input exactly where it was.
   (It used to be a space-between flex row with align-items:center, so every
   one of those changed the input's vertical position.) */
.controls-row{
  display:grid; grid-template-columns:minmax(0, 360px) minmax(0, 1fr);
  align-items:start; gap:16px; margin-bottom:10px;
}
/* Same input as Admin > Storage (admin.css .storage-search): the generic
   theme.css input rule, capped to a sane width and pinned to the row height. */
.controls-row .browse-search{
  width:100%; max-width:360px; height:var(--control-h);
  padding-top:0; padding-bottom:0;
}
.controls-row .browse-filters{margin-bottom:0;}
@media (max-width:760px){
  /* Stacked, so the input keeps a usable width instead of a 220px stub. */
  .controls-row{grid-template-columns:minmax(0, 1fr);}
  .controls-row .browse-search{max-width:none;}
}

/* ============================================================================
   The catalog index (.track-index)

   Replaces the third identical card rail. Reading a catalog is reading a
   tracklist, so it is numbered and rule-separated like one - and the column
   that was empty in the row layout (nothing between the title and the BPM)
   now carries that track's waveform. waveform.js already computes these for
   the transport bar; here every row wears one.
   ========================================================================== */
.track-index{border-top:1px solid var(--line-strong);}
.tx-row{
  display:grid;
  grid-template-columns:46px 56px minmax(150px, 210px) minmax(0, 1fr) 92px 88px 112px;
  align-items:center; gap:20px;
  padding:14px 16px; border-bottom:1px solid var(--line);
  transition:background 160ms ease;
}
/* /tracks keeps a licence picker on every row; the homepage index opens the
   licence modal instead, so it does not pay for the column. */
.track-index.has-license .tx-row{grid-template-columns:46px 56px minmax(150px, 210px) minmax(0, 1fr) 92px 88px 264px;}
.tx-row:hover{background:rgba(255,255,255,0.02);}
.tx-num{font-family:var(--font-mono); font-size:13px; color:var(--text-faint); letter-spacing:0.04em;}
.tx-art{position:relative; width:56px; height:56px; border-radius:8px; overflow:hidden; background:var(--art-fallback); border:1px solid var(--line); cursor:pointer;}
.tx-art img{position:absolute; inset:0; width:100%; height:100%; object-fit:cover;}
/* The play control is visible at rest - a hover-only affordance is invisible
   on a phone, where most of the catalog is browsed. */
.tx-play{
  position:absolute; left:50%; top:50%; transform:translate(-50%, -50%);
  width:26px; height:26px; border-radius:50%;
  background:rgba(5,5,5,0.7); color:var(--text); border:none;
  display:flex; align-items:center; justify-content:center;
  transition:background 150ms, color 150ms, transform 150ms;
}
.tx-art:hover .tx-play{transform:translate(-50%, -50%) scale(1.08);}
.tx-play svg{width:11px; height:11px;}
.tx-play .ic-pause{display:none;}
.tx-main{min-width:0;}
.tx-title{display:block; font-size:15px; font-weight:600; overflow:hidden; text-overflow:ellipsis; white-space:nowrap;}
.tx-title:hover{color:var(--theme-text-color);}
.tx-sub{font-family:var(--font-mono); font-size:11px; color:var(--text-faint); margin-top:2px; overflow:hidden; text-overflow:ellipsis; white-space:nowrap;}
.tx-wave{min-width:0;}
.tx-wave canvas{display:block; width:100%; height:34px; cursor:pointer;}
.tx-spec{font-family:var(--font-mono); font-size:12px; color:var(--text-dim); text-align:right; line-height:1.35; white-space:nowrap;}
.tx-spec small{display:block; font-size:12px; color:var(--text-faint);}
.tx-price{font-family:var(--font-mono); font-size:14px; text-align:right; white-space:nowrap;}
.tx-price .free{color:var(--success);}
.tx-buy{display:flex; align-items:center; gap:8px; min-width:0;}
.tx-license{flex:1; min-width:0;}
.tx-license select{
  width:100%; padding:8px 10px; font-size:11px; font-family:var(--font-mono);
  border-radius:999px; background:var(--panel-2); border:1px solid var(--line-strong); color:var(--text);
}
.tx-action{
  border-radius:999px; padding:9px 16px; flex-shrink:0; white-space:nowrap;
  font-family:var(--font-mono); font-size:11px; text-transform:uppercase; letter-spacing:0.05em;
  background:transparent; border:1px solid var(--line-strong); color:var(--text);
  transition:border-color 150ms, color 150ms;
}
/* The end-of-row actions are one control in three moods — License on a paid
   row, Download on a free one, MP3 on a pack — so they are one size. Sizing
   them by their own label is what used to leave a ragged right edge, since
   "License" and "Download" are not the same number of characters. */
.tx-action[data-tx-license],
.tx-action.tx-dl{
  display:inline-flex; align-items:center; justify-content:center;
  min-width:112px; text-align:center;
}
.tx-action:hover{border-color:var(--theme-color); color:var(--theme-text-color);}
/* The phone-only twin of the licence picker — see the @640 block below. */
.tx-action.tx-compact{display:none;}
/* a free track is the one thing on the row worth a filled button */
.tx-action.is-free{background:var(--text); border-color:var(--text); color:var(--bg);}
.tx-action.is-free:hover{opacity:0.86; color:var(--bg);}
/* A free row carries no licence picker, so its lone Download button used to
   start at the left edge of the buy cell while every paid row's Add button
   sits flush right after the select — one column, two vertical lines. The
   lone button takes the picker's place so the column reads as a single edge
   whatever the row is selling. */
.track-index.has-license .tx-buy > .tx-action:only-child{margin-left:auto;}

/* A pack's rows carry an MP3 link as well as License. Both slots are the same
   fixed width and the cell is pushed right, so MP3, License and the price
   above them read as three straight columns down the list. */
.track-index.has-dl .tx-row{grid-template-columns:46px 56px minmax(150px, 210px) minmax(0, 1fr) 92px 88px 240px;}
.track-index.has-dl .tx-buy{justify-content:flex-end;}
.track-index.has-dl .tx-buy > .tx-action{flex:0 0 112px;}

/* the row you are listening to, marked without moving anything */
.tx-row.is-current{
  background:linear-gradient(90deg, var(--accent-soft), transparent 60%);
  box-shadow:inset 3px 0 0 var(--theme-color);
}
.tx-row.is-current .tx-num{color:var(--theme-text-color);}
.tx-row.is-current .tx-title{color:var(--theme-text-color);}
.tx-row.is-current .tx-play{background:var(--theme-color); color:var(--theme-on-color);}
.tx-row.is-playing .tx-play .ic-play{display:none;}
.tx-row.is-playing .tx-play .ic-pause{display:block;}

/* Columns drop in order of how little they carry - BPM and key also read in
   the line under the title, so they go first. */
@media (max-width:1040px){
  .tx-row{grid-template-columns:40px 52px minmax(130px, 190px) minmax(0, 1fr) 84px 104px; gap:16px;}
  .track-index.has-dl .tx-row{grid-template-columns:40px 52px minmax(120px, 170px) minmax(0, 1fr) 84px 232px;}
  .track-index.has-license .tx-row{grid-template-columns:40px 52px minmax(120px, 170px) minmax(0, 1fr) 84px 232px;}
  .tx-spec{display:none;}
}
@media (max-width:840px){
  .tx-row{grid-template-columns:36px 52px minmax(0, 1fr) 84px 104px; gap:14px;}
  .track-index.has-dl .tx-row{grid-template-columns:36px 52px minmax(0, 1fr) 84px 232px;}
  .track-index.has-license .tx-row{grid-template-columns:36px 52px minmax(0, 1fr) 84px 210px;}
  .tx-wave{display:none;}
}
@media (max-width:640px){
  /* Two tiers rather than a squashed grid: identity on top, then the waveform
     and the buy button side by side on the second line — the wave takes what
     the button leaves, so the row is two lines instead of three and the
     action stays on the same line as the beat it belongs to. */
  .track-index{border-top:none;}
  /* The .has-license selector has to be repeated here: it is a two-class
     rule, so the 840px column template outranks a bare .tx-row however late
     this block comes — and four area names against five columns leaves the
     title cell at 0px wide. */
  .tx-row,
  .track-index.has-dl .tx-row,
  .track-index.has-license .tx-row{
    grid-template-columns:24px 48px minmax(0, 1fr) auto;
    justify-items:stretch;
    grid-template-areas:"num art main price" "wave wave wave action";
    row-gap:10px; column-gap:12px; padding:14px 0;
  }
  .tx-num{grid-area:num; font-size:12px;}
  .tx-art{grid-area:art; width:48px; height:48px;}
  .tx-main{grid-area:main;}
  .tx-price{grid-area:price;}
  .tx-wave{grid-area:wave; display:block;}
  .tx-wave canvas{height:30px;}
  /* The button sits in the price's column, so the two share a right edge
     whatever the row is selling. */
  .tx-buy{grid-area:action; justify-content:flex-end;}
  /* One line means one control. A licence picker plus an Add button is ~215px
     of the ~330px a phone has, which would leave the waveform a stub, and a
     <select> set in 11px mono is the worst of the two to hit with a thumb.
     So /tracks drops to the same licence modal the homepage index uses —
     bigger targets, and the tier descriptions come with it. */
  .track-index.has-license .tx-license,
  .track-index.has-license .tx-buy > [data-tx-add]{display:none;}
  .tx-action.tx-compact{display:inline-block;}
  .tx-action{padding:12px 18px;}
  /* Two actions have to share one line on a phone, so they shrink together
     rather than one of them wrapping. */
  .tx-action[data-tx-license],
  .tx-action.tx-dl{min-width:96px;}
  .tx-row.is-current{margin:0 -12px; padding:14px 12px;}
}

/* ---------- badges/tags ---------- */
.badge-pill{display:inline-block; background:var(--text); color:var(--bg); font-size:11px; font-weight:700; border-radius:999px; padding:3px 10px;}
/* A kit's track count at display size, ghosted into the cover. Shared so the
   homepage rail and /kits show the same card. */
/* Which license a kit is sold under, on the card itself: half of what the
   price means. Reads at the weight of the taxonomy tags, not the title. */
.kit-lic{display:flex; align-items:center; gap:6px; font-size:11px; color:var(--theme-text-color); margin:0 0 10px; line-height:1.4; min-width:0;}
.kit-lic svg{width:11px; height:11px; flex-shrink:0;}
.kit-lic span{overflow:hidden; text-overflow:ellipsis; white-space:nowrap;}

.kit-count-big{
  position:absolute; right:12px; bottom:8px; z-index:2; pointer-events:none;
  font-family:var(--font-head); font-style:italic; font-weight:900; font-size:40px;
  line-height:1; color:rgba(245,243,239,0.14);
}
.tag{display:inline-block; font-family:var(--font-mono); font-size:11px; color:var(--text-dim); border:1px solid var(--line-strong); border-radius:999px; padding:3px 10px;}
.tag.accent{color:var(--accent); border-color:var(--accent-dim);}

/* ---------- toasts ---------- */
.toast-stack{position:fixed; bottom:24px; left:24px; display:flex; flex-direction:column; gap:10px; z-index:200;}
.toast{background:var(--panel); border:1px solid var(--line-strong); border-left:3px solid var(--accent); border-radius:8px; padding:12px 16px; font-size:13px; box-shadow:0 8px 24px rgba(0,0,0,0.5); max-width:320px; animation:toast-in 200ms ease;}
.toast.error{border-left-color:var(--danger);}
.toast.success{border-left-color:var(--success);}
@keyframes toast-in{from{opacity:0; transform:translateY(8px);} to{opacity:1; transform:translateY(0);}}

/* ---------- cookie consent: banner + preference centre ---------- */
.cookie-banner{
  position:fixed; left:16px; bottom:16px; z-index:300; width:min(460px, calc(100vw - 32px));
  background:var(--panel); border:1px solid var(--line-strong); border-radius:var(--radius-lg);
  padding:18px 20px; box-shadow:0 12px 32px rgba(0,0,0,0.55); display:none; flex-direction:column; gap:14px;
  animation:cookie-in 240ms ease;
}
.cookie-banner.open{display:flex;}
@keyframes cookie-in{from{opacity:0; transform:translateY(12px);} to{opacity:1; transform:translateY(0);}}
.cookie-banner h4{margin:0 0 6px; font-family:var(--font-head); font-style:italic; font-weight:800; font-size:16px;}
.cookie-banner p{margin:0; font-size:12px; color:var(--text-dim); line-height:1.6;}
.cookie-banner-actions{display:flex; gap:8px; flex-wrap:wrap;}
.cookie-btn-sm{padding:9px 14px; font-size:12px; border-radius:999px;}
.cookie-banner-actions .btn-primary{margin-left:auto;}
/* The banner sits above the transport bar rather than under it. */
body.has-player .cookie-banner{bottom:96px;}

/* Above the cart card (71) and the transport bar (50). Two classes on
   purpose: pages restate a plain .overlay rule in their own <style>, which
   would otherwise win on source order. */
.overlay.cookie-overlay{z-index:310;}
.cookie-overlay .cookie-modal{
  background:var(--panel); border:1px solid var(--line-strong); border-radius:var(--radius-lg);
  width:100%; max-width:560px; max-height:86vh; overflow-y:auto; padding:24px;
}
.cookie-modal-head{display:flex; justify-content:space-between; align-items:center; margin-bottom:18px;}
.cookie-modal-head h3{font-size:20px;}
.cookie-cat{display:flex; gap:16px; align-items:flex-start; padding:14px 0; border-top:1px solid var(--line);}
.cookie-cat-text{flex:1;}
.cookie-cat-name{font-weight:600; font-size:14px; margin-bottom:4px;}
.cookie-cat p{margin:0; font-size:12px; color:var(--text-dim); line-height:1.55;}
.cookie-switch{position:relative; width:42px; height:24px; flex-shrink:0; margin:2px 0 0;}
.cookie-switch input{position:absolute; opacity:0; width:100%; height:100%; margin:0; cursor:pointer;}
.cookie-switch span{position:absolute; inset:0; background:var(--panel-2); border:1px solid var(--line-strong); border-radius:999px; transition:background 150ms, border-color 150ms; pointer-events:none;}
.cookie-switch span::after{content:""; position:absolute; top:3px; left:3px; width:16px; height:16px; border-radius:50%; background:var(--text-faint); transition:transform 150ms, background 150ms;}
.cookie-switch input:checked + span{background:var(--accent-soft); border-color:var(--accent);}
.cookie-switch input:checked + span::after{transform:translateX(18px); background:var(--accent);}
.cookie-switch.is-locked{opacity:0.7;}
.cookie-modal-foot{display:flex; gap:10px; justify-content:flex-end; margin-top:20px; flex-wrap:wrap;}
.cookie-reopen{display:inline; background:none; border:none; padding:0 0 0 10px; color:var(--text-faint); font-size:11px; text-decoration:underline; font-family:var(--font-body);}
.cookie-reopen:hover{color:var(--accent);}

/* ---------- modal overlay (shared; storefront pages may restate it) ---------- */
.overlay{position:fixed; inset:0; background:rgba(0,0,0,0.8); display:none; align-items:center; justify-content:center; z-index:60; padding:20px;}
.overlay.open{display:flex;}
.modal-close{background:transparent; border:none; color:var(--text-dim); font-size:20px;}
.modal-close:hover{color:var(--text);}

/* ---------- uniform square artwork ----------
   Every track/kit thumbnail on the site goes through .art-sq: a strict 1:1
   box with object-fit:cover, so a portrait, a landscape and a tiny 200px
   cover all render identically and never change a card's height. Cards in a
   row therefore stay aligned whatever the source images are. */
.art-sq{position:relative; width:100%; aspect-ratio:1 / 1; overflow:hidden; border-radius:12px; background:var(--art-fallback); flex-shrink:0;}
.art-sq > img{position:absolute; inset:0; width:100%; height:100%; object-fit:cover; transition:transform 320ms ease;}
/* Fallback for a track with no artwork uploaded — themed placeholder + code,
   never a broken image or a collapsed box. */
.art-sq .art-empty{position:absolute; inset:0; display:flex; flex-direction:column; align-items:center; justify-content:center; gap:6px; color:var(--text-faint); font-family:var(--font-mono); font-size:11px; letter-spacing:0.08em;}
.art-sq .art-empty svg{width:24px; height:24px; opacity:0.55;}
.art-sq .art-scrim{position:absolute; inset:0; background:linear-gradient(180deg, rgba(0,0,0,0.10) 30%, rgba(0,0,0,0.55)); opacity:0; transition:opacity 180ms ease; pointer-events:none;}

/* interactive states: inactive → hover → playing → paused */
.art-sq.is-clickable{cursor:pointer;}
.art-sq.is-clickable:hover > img{transform:scale(1.05) rotate(-2deg);}
.art-sq.is-clickable:hover .art-scrim{opacity:1;}
.art-sq.is-current .art-scrim{opacity:1;}
.art-sq.is-current{box-shadow:inset 0 0 0 2px var(--accent);}
/* Centered on the cover, not tucked into a corner: it is the one thing the
   whole thumbnail does, so it reads as the target for the whole square.
   The centering itself lives in the transform, which is also what the
   reveal animates — both halves have to be written together or the button
   snaps back to the top-left corner mid-transition. */
.art-play{
  position:absolute; top:50%; left:50%;
  width:40px; height:40px; border-radius:50%;
  background:var(--accent); color:var(--accent-ink); border:none;
  display:flex; align-items:center; justify-content:center;
  /* Visible at rest, not only on hover: on a phone there is no hover, and a
     cover with no affordance on it reads as a picture rather than a player.
     Hover still lifts it to full strength. */
  opacity:0.92; transform:translate(-50%, -50%) scale(0.86);
  transition:opacity 160ms ease, transform 160ms ease; pointer-events:none;
}
.art-play svg{width:16px; height:16px;}
.art-play .ic-pause{display:none;}
.art-sq.is-playing .art-play .ic-play{display:none;}
.art-sq.is-playing .art-play .ic-pause{display:block;}
.art-sq.is-clickable:hover .art-play, .art-sq.is-current .art-play{opacity:1; transform:translate(-50%, -50%) scale(1);}
/* equalizer only animates while actually playing — a paused track keeps the
   active outline but the bars freeze. */
.art-eq{position:absolute; bottom:14px; right:10px; display:none; align-items:flex-end; gap:2px; height:15px;}
.art-sq.is-current .art-eq{display:flex;}
.art-eq span{width:2.5px; border-radius:1px; background:var(--accent); height:30%;}
.art-sq.is-playing .art-eq span{animation:eq-bounce 700ms ease-in-out infinite;}
.art-eq span:nth-child(2){animation-delay:120ms;}
.art-eq span:nth-child(3){animation-delay:240ms;}
.art-eq span:nth-child(4){animation-delay:80ms;}
@keyframes eq-bounce{0%,100%{height:25%;} 50%{height:100%;}}

/* ---------- global transport bar (markup injected by player.js) ---------- */
.mini-player{
  position:fixed; left:0; right:0; bottom:0; z-index:50;
  background:rgba(10,10,10,0.96); backdrop-filter:blur(8px);
  border-top:1px solid var(--line-strong); padding:10px 20px;
  display:flex; align-items:center; gap:14px;
  transform:translateY(110%); transition:transform 260ms ease;
}
.mini-player.open{transform:translateY(0);}
.mini-player .mp-art{width:44px; height:44px; border-radius:8px; overflow:hidden; background:var(--art-fallback); border:1px solid var(--line); flex-shrink:0; padding:0; display:flex; align-items:center; justify-content:center; color:var(--text-faint); font-size:10px;}
.mini-player .mp-art img{width:100%; height:100%; object-fit:cover;}
.mp-transport{display:flex; align-items:center; gap:6px; flex-shrink:0;}
.mp-nav{width:32px; height:32px; border-radius:50%; border:none; background:transparent; color:var(--text-dim); display:flex; align-items:center; justify-content:center;}
.mp-nav svg{width:16px; height:16px;}
.mp-nav:hover:not(:disabled){color:var(--text);}
.mp-nav:disabled{opacity:0.3; cursor:not-allowed;}
.mp-play{width:40px; height:40px; border-radius:50%; border:1.5px solid var(--accent); background:transparent; color:var(--accent); display:flex; align-items:center; justify-content:center; flex-shrink:0; transition:background 150ms, color 150ms;}
.mp-play:hover{background:var(--accent); color:var(--accent-ink);}
.mp-play svg{width:16px; height:16px;}
.mp-play .ic-pause{display:none;}
.mini-player.is-playing .mp-play .ic-play{display:none;}
.mini-player.is-playing .mp-play .ic-pause{display:block;}
.mini-player .mp-meta{min-width:120px; max-width:200px; flex-shrink:0;}
.mini-player .mp-meta .title{font-weight:600; font-size:14px; white-space:nowrap; overflow:hidden; text-overflow:ellipsis;}
.mini-player .mp-meta .sub{font-family:var(--font-mono); font-size:11px; color:var(--text-faint); white-space:nowrap; overflow:hidden; text-overflow:ellipsis;}
.mp-wave-wrap{flex:1; min-width:80px;}
.mp-wave{display:block; width:100%; height:38px; cursor:pointer; touch-action:none;}
.mp-time{font-family:var(--font-mono); font-size:11px; color:var(--text-faint); min-width:84px; text-align:right; flex-shrink:0;}
.mp-vol{display:flex; align-items:center; gap:8px; width:110px; flex-shrink:0; color:var(--text-faint);}
.mp-vol svg{width:16px; height:16px; flex-shrink:0;}
.mp-vol input[type=range]{width:100%; padding:0; height:4px; background:var(--line-strong); border:none; border-radius:2px; appearance:none; -webkit-appearance:none;}
.mp-vol input[type=range]::-webkit-slider-thumb{appearance:none; -webkit-appearance:none; width:12px; height:12px; border-radius:50%; background:var(--text-dim); cursor:pointer;}
.mp-vol input[type=range]::-moz-range-thumb{width:12px; height:12px; border:none; border-radius:50%; background:var(--text-dim); cursor:pointer;}
.mp-close{background:none; border:none; color:var(--text-faint); font-size:15px; flex-shrink:0; padding:4px;}
.mp-close:hover{color:var(--text);}
body.has-player{padding-bottom:76px;}

@media (max-width:900px){
  .mini-player{flex-wrap:wrap; gap:10px 12px; padding:8px 14px 10px;}
  .mp-vol{display:none;}
  .mini-player .mp-meta{flex:1; max-width:none; min-width:0;}
  .mp-wave-wrap{order:5; flex-basis:100%; min-width:100%;}
  /* big enough to scrub with a thumb */
  .mp-wave{height:44px;}
  .mp-time{order:4; min-width:0;}
  body.has-player{padding-bottom:132px;}
}

/* ============================================================================
   Horizontal rail — the only way tracks/kits are browsed in a row: scroll,
   trackpad, swipe, or the two arrows. No vertical side panel, and vertical
   scrolling is explicitly disabled inside the rail so a stray wheel gesture
   can't hide cards. Arrows are hidden when there's nothing to scroll to.
   ========================================================================== */
.rail{position:relative;}
.rail > .grid, .rail > .featured-row, .rail > .kits-row{
  overflow-x:auto; overflow-y:hidden;
  scroll-behavior:smooth; overscroll-behavior-x:contain;
  -webkit-overflow-scrolling:touch;
}
.rail-arrow{
  position:absolute; top:38%; transform:translateY(-50%); z-index:6;
  width:38px; height:38px; border-radius:50%;
  background:rgba(10,10,10,0.86); border:1px solid var(--line-strong); color:var(--text);
  display:flex; align-items:center; justify-content:center;
  box-shadow:0 6px 18px rgba(0,0,0,0.45);
  transition:opacity 160ms ease, border-color 150ms, color 150ms;
}
.rail-arrow svg{width:16px; height:16px;}
.rail-arrow:hover{border-color:var(--theme-color); color:var(--theme-text-color);}
.rail-arrow.prev{left:-6px;}
.rail-arrow.next{right:-6px;}
.rail-arrow[hidden]{display:none;}
/* Touch devices scroll with a finger — the arrows would just cover a card. */
@media (hover:none){ .rail-arrow{display:none;} }

/* ============================================================================
   Cart / payment-validation card (cartcard.js). One row template for every
   page: artwork, title, BPM + key, license chip, price, trash button.
   Every colour here is derived from the theme accent — nothing hardcoded.
   ========================================================================== */

/* ---------- wallet look ----------
   Replaces the old uploadable background image. Drawn entirely in CSS: a
   leather-ish body with a stitched edge, a front flap, and two banknotes
   peeking out of the top. Purely decorative (pointer-events:none, aria
   hidden by virtue of being pseudo-elements) so it can never sit between
   the visitor and the cart contents. */
.cart-card{
  background:
    linear-gradient(160deg, color-mix(in srgb, var(--panel) 92%, #ffffff 8%), var(--panel) 45%, #0c0c0c 100%);
  box-shadow:
    0 24px 54px rgba(0,0,0,0.55),
    inset 0 1px 0 rgba(255,255,255,0.06),
    inset 0 -18px 34px rgba(0,0,0,0.35);
}
/* the banknotes, tucked behind the card body and clipped by its rounding.
   A single SVG graphic (two fanned bills, engraved portrait oval + $ mark)
   instead of flat colour blocks — reads as cash, not as green rectangles. */
.cart-card::before{
  content:""; position:absolute; left:22px; right:22px; top:-30px; height:52px;
  z-index:0; pointer-events:none;
  background:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 320 100'%3E%3Cg transform='rotate(-7 90 66)'%3E%3Crect x='14' y='30' width='150' height='64' rx='7' fill='%233c7a4e' stroke='%23e9e4d6' stroke-width='2'/%3E%3Crect x='22' y='38' width='134' height='48' rx='3' fill='none' stroke='%236fae80' stroke-width='1'/%3E%3Ccircle cx='89' cy='62' r='17' fill='none' stroke='%23e9e4d6' stroke-width='1.4'/%3E%3Ctext x='89' y='69' font-family='Georgia,serif' font-size='18' fill='%23e9e4d6' text-anchor='middle'%3E%24%3C/text%3E%3Ctext x='30' y='50' font-family='Georgia,serif' font-size='11' fill='%23bfe0c9'%3E100%3C/text%3E%3Ctext x='128' y='80' font-family='Georgia,serif' font-size='11' fill='%23bfe0c9'%3E100%3C/text%3E%3C/g%3E%3Cg transform='rotate(6 235 60)'%3E%3Crect x='176' y='22' width='140' height='60' rx='7' fill='%23477f57' stroke='%23e9e4d6' stroke-width='2'/%3E%3Crect x='184' y='30' width='124' height='44' rx='3' fill='none' stroke='%2378b487' stroke-width='1'/%3E%3Ccircle cx='246' cy='52' r='15' fill='none' stroke='%23e9e4d6' stroke-width='1.4'/%3E%3Ctext x='246' y='58' font-family='Georgia,serif' font-size='16' fill='%23e9e4d6' text-anchor='middle'%3E%24%3C/text%3E%3Ctext x='188' y='40' font-family='Georgia,serif' font-size='10' fill='%23c6e2cf'%3E50%3C/text%3E%3Ctext x='290' y='72' font-family='Georgia,serif' font-size='10' fill='%23c6e2cf'%3E50%3C/text%3E%3C/g%3E%3C/svg%3E");
  background-repeat:no-repeat;
  background-position:center bottom;
  background-size:auto 100%;
  filter:drop-shadow(0 4px 8px rgba(0,0,0,0.4));
}
/* the wallet's front flap + its stitching, drawn over the lower edge */
.cart-card::after{
  content:""; position:absolute; left:0; right:0; bottom:0; height:54%;
  z-index:0; pointer-events:none;
  border-radius:0 0 18px 18px;
  background:linear-gradient(180deg, rgba(255,255,255,0.03), rgba(0,0,0,0.30));
  border-top:1px solid rgba(255,255,255,0.07);
  box-shadow:inset 0 1px 0 rgba(255,255,255,0.04);
}
/* content sits above both decorations */
.cart-card > *{position:relative; z-index:1;}
/* the stitched border of the wallet. The doubled class beats the per-page
   .cart-card rule (which sets overflow:hidden and would clip the notes). */
.cart-card.cart-card{border:1px solid var(--line-strong); outline:1px dashed rgba(255,255,255,0.07); outline-offset:-7px; overflow:visible;}
/* the item list keeps its own scroll, so nothing else needs clipping */
.cart-card #cartItems{overflow-y:auto; overflow-x:hidden;}
@media (max-width:420px){
  .cart-card::before{left:16px; right:16px; top:-22px; height:38px;}
}
@media (prefers-reduced-motion:reduce){ .cart-card::before{transform:none;} }
.cart-item{
  display:grid; grid-template-columns:56px 1fr auto; gap:6px 12px; align-items:center;
  padding:12px 0; border-bottom:1px solid var(--line);
}
.cart-item:last-child{border-bottom:none;}
.ci-art{width:56px; border-radius:8px; grid-row:1 / 2;}
.ci-main{min-width:0;}
.ci-title{font-weight:600; font-size:14px; line-height:1.3; overflow:hidden; text-overflow:ellipsis; white-space:nowrap;}
.ci-spec{font-size:11px; color:var(--text-faint); margin:2px 0 0; overflow:hidden; text-overflow:ellipsis; white-space:nowrap;}
/* License now sits on its own row, clear of the title/spec block — see
   the item() template in cartcard.js. Indented under ci-main (skips the
   artwork column) so it reads as an attribute of the track, not the row. */
.ci-license{grid-column:2 / -1;}
/* License badge — accent-tinted surface with accent-tinted text, both
   computed from --accent, so it stays legible and on-brand in any theme. */
.license-chip{
  display:inline-block; font-family:var(--font-mono); font-size:10px;
  text-transform:uppercase; letter-spacing:0.05em; padding:3px 9px; border-radius:999px;
  background:var(--accent-soft); border:1px solid var(--accent-dim); color:var(--theme-text-color);
}
.license-chip.kit{background:transparent; border-color:var(--line-strong); color:var(--text-dim);}
.ci-right{display:flex; align-items:center; gap:10px; flex-shrink:0; grid-row:1 / 2;}
.ci-price{font-size:13px; white-space:nowrap;}
.ci-remove{
  width:34px; height:34px; flex-shrink:0; border-radius:8px;
  background:transparent; border:1px solid transparent; color:var(--text-faint);
  display:flex; align-items:center; justify-content:center;
  transition:color 150ms, background 150ms, border-color 150ms;
}
.ci-remove svg{width:17px; height:17px;}
.ci-remove:hover{color:var(--danger); background:rgba(229,72,77,0.10); border-color:rgba(229,72,77,0.35);}
.ci-remove:focus-visible{outline:2px solid var(--danger); outline-offset:2px;}
@media (max-width:420px){
  .cart-item{grid-template-columns:44px 1fr auto; gap:10px;}
  .ci-art{width:44px;}
}

/* Admin-only upload inside the open cart card (the image behind it). */
.cc-upload{border-top:1px solid var(--line-strong); margin-top:14px; padding-top:12px; flex-shrink:0;}
.cc-upload-row{display:flex; gap:8px; flex-wrap:wrap;}
.cc-upload-btn{padding:7px 12px; font-size:11px; border-radius:999px;}
.cc-upload-status{font-size:11px; color:var(--text-faint); margin:8px 0 0; line-height:1.5;}
.upload-bar{height:3px; background:var(--line); border-radius:2px; margin-top:8px; overflow:hidden; display:none;}
.upload-bar.active{display:block;}
.upload-bar-fill{height:100%; width:0%; background:var(--theme-color); transition:width 120ms linear;}

/* ---------- audio playlist rows (kit + pack pages) ---------- */
.playlist{border:1px solid var(--line); border-radius:var(--radius-lg); overflow:hidden;}
.pl-row{display:grid; grid-template-columns:34px 48px 1fr auto; gap:14px; align-items:center; padding:10px 14px; border-bottom:1px solid var(--line); transition:background 150ms ease;}
.pl-row:last-child{border-bottom:none;}
.pl-row:hover{background:var(--panel);}
.pl-row.is-current{background:color-mix(in srgb, var(--accent) 10%, transparent);}
.pl-num{font-family:var(--font-mono); font-size:12px; color:var(--text-faint); text-align:right;}
.pl-row.is-current .pl-num{color:var(--accent);}
.pl-art{width:48px;}
.pl-art .art-sq{border-radius:8px;}
.pl-main{min-width:0;}
.pl-title{font-weight:600; font-size:14px; white-space:nowrap; overflow:hidden; text-overflow:ellipsis;}
.pl-sub{font-family:var(--font-mono); font-size:11px; color:var(--text-faint); white-space:nowrap; overflow:hidden; text-overflow:ellipsis;}
.pl-right{display:flex; align-items:center; gap:12px; flex-shrink:0;}
.pl-dur{font-family:var(--font-mono); font-size:11px; color:var(--text-faint); min-width:38px; text-align:right;}
@media (max-width:640px){
  .pl-row{grid-template-columns:26px 44px 1fr auto; gap:10px; padding:9px 10px;}
  .pl-art{width:44px;}
  .pl-dur{display:none;}
}

/* ---------- generic form controls (shared by admin + account forms) ---------- */
input, select, textarea{
  background:var(--panel-2); border:1px solid var(--line-strong); color:var(--text);
  border-radius:var(--radius); padding:10px 12px; font-size:13px; font-family:var(--font-body); width:100%;
}
input:focus, select:focus, textarea:focus{outline:2px solid var(--accent); outline-offset:1px;}
label{display:block; font-size:12px; color:var(--text-dim); margin-bottom:14px;}
label > input, label > select, label > textarea{margin-top:6px;}

/* ============================================================================
   Page + section titles — ONE typographic scale.
   /tracks used a 28px .section-heading while /kits used a clamped 30→48px
   h1, so the two pages looked like different products. Both now use
   .page-title; a section inside a page uses .section-heading, which is a
   deliberate step down from it rather than an unrelated size.
   ========================================================================== */
.page-title, .section-heading{
  font-family:var(--font-head); font-style:italic; font-weight:800;
  letter-spacing:-0.01em; line-height:1.1;
}
/* Display scale. Archivo italic is the strongest thing the brand owns and it
   was being used at 28px for section headings against a 68px hero — a hole in
   the middle of the ramp. Both ends move up together. */
.page-title{font-size:clamp(38px, 5.4vw, 64px); margin:0 0 18px; letter-spacing:-0.03em; line-height:1;}
.section-heading{font-size:clamp(30px, 3.4vw, 40px); margin:0 0 18px; letter-spacing:-0.02em; line-height:1;}

/* The small mono line that opens a page or a section. */
.eyebrow{
  display:flex; align-items:center; gap:8px;
  font-family:var(--font-mono); font-size:11px; text-transform:uppercase;
  letter-spacing:0.12em; color:var(--text-faint); margin-bottom:18px;
}
.eyebrow .dot{width:6px; height:6px; background:var(--theme-color); flex-shrink:0;}

/* A section title with something on its right (rail arrows, a link). */
.section-head{display:flex; align-items:flex-end; justify-content:space-between; gap:20px; margin-bottom:20px;}
.section-head .section-heading{margin:0;}

/* ============================================================================
   Track card, row layout (.beat-card.card-row)
   Used by list pages (/tracks). One line on desktop, and every parameter is
   its own grid column so they line up down the entire list:
     [artwork] [title + specs] [BPM] [price] [licence] [add]
   They used to be a right-aligned flex stack (BPM in one column, then price,
   select and button piled inside a single "buy" cell), which meant each row
   placed them wherever its own content ended — nothing lined up with the row
   above it. The cells are fixed-width and the controls fill them, so the
   columns hold whatever the price, the licence name or the button label is.
   The carousels on the homepage keep the compact vertical card — a row card
   cannot work in a five-across rail.
   ========================================================================== */
.beat-card.card-row{
  display:grid;
  grid-template-columns:72px minmax(0, 1fr) 72px 96px 168px 116px;
  align-items:center; gap:16px;
  padding:12px 14px;
  background:var(--panel); border:1px solid var(--line); border-radius:12px;
  /* every row is the same height whatever the title length */
  min-height:96px;
  transition:border-color 160ms ease;
}
.beat-card.card-row:hover{border-color:var(--line-strong);}
.beat-card.card-row .beat-art{width:72px; margin:0;}
.card-row .row-main{min-width:0;}
/* long titles get an ellipsis instead of wrapping the row open */
.card-row .beat-title{
  font-size:15px; font-weight:600; display:block; margin:0;
  overflow:hidden; text-overflow:ellipsis; white-space:nowrap;
}
.card-row .row-specs{
  font-family:var(--font-mono); font-size:11px; color:var(--text-faint);
  margin-top:3px; overflow:hidden; text-overflow:ellipsis; white-space:nowrap;
}
.card-row .row-bpm{
  font-family:var(--font-mono); font-size:13px; color:var(--text-dim);
  text-align:center; white-space:nowrap;
}
.card-row .row-bpm small{display:block; font-size:9px; letter-spacing:0.08em; text-transform:uppercase; color:var(--text-faint);}
/* Right-aligned and tabular so the digits themselves stack, not just the
   boxes around them. */
.card-row .row-price{
  font-family:var(--font-mono); font-size:16px; white-space:nowrap;
  text-align:right; font-variant-numeric:tabular-nums;
}
.card-row .row-price .free{color:var(--success);}
/* The licence cell keeps its width even when a row has no picker (a free
   track), which is what stops that row's button drifting left. */
.card-row .row-license{min-width:0;}
.card-row .license-select{
  width:100%; padding:6px 10px; font-size:11px;
  font-family:var(--font-mono); border-radius:999px;
}
.card-row .row-action{display:flex; justify-content:flex-end;}
.card-row .license-btn{width:100%; padding:8px 12px; font-size:11px; text-align:center;}

/* the free-download badge sits on the cover, which is small here */
.card-row .beat-art .free-badge{top:4px; right:4px; font-size:9px; padding:2px 7px;}
.card-row .beat-art .overlay-tag{display:none;}

/* Columns are dropped in order of how little they carry, so what is left
   stays aligned instead of everything compressing at once. */
@media (max-width:1040px){
  .beat-card.card-row{grid-template-columns:72px minmax(0, 1fr) 64px 92px 150px 108px; gap:12px;}
}
@media (max-width:940px){
  /* BPM also reads in the specs line under the title. */
  .beat-card.card-row{grid-template-columns:64px minmax(0, 1fr) 92px 150px 108px;}
  .card-row .row-bpm{display:none;}
  .beat-card.card-row .beat-art{width:64px;}
}
@media (max-width:780px){
  .beat-card.card-row{grid-template-columns:64px minmax(0, 1fr) 90px 108px;}
  .card-row .row-license{display:none;}
}
@media (max-width:640px){
  /* On a phone the row stops being a grid. Keeping the two columns put the
     price, the licence picker and the button each on its own line inside
     column 2, so a card climbed like a staircase and a list of ten filled
     three screens. Flex instead: the cover and the title sit on the first
     line, then a hairline, then ONE action line — price on the left,
     licence + Add pushed to the right. `order` is what puts that hairline
     (the card's ::after, so no extra markup) between the two groups. */
  .beat-card.card-row{
    display:flex; flex-wrap:wrap; align-items:center;
    gap:0; padding:12px; min-height:0;
  }
  .beat-card.card-row .beat-art{order:0; flex:0 0 64px; width:64px; margin-right:12px;}
  .card-row .row-main{order:0; flex:1 1 0;}
  .card-row .beat-title{white-space:normal;}
  .card-row .row-specs{white-space:normal;}
  .beat-card.card-row::after{
    content:""; order:1; flex:0 0 100%;
    height:1px; background:var(--line); margin:12px 0;
  }
  /* margin-right:auto is the whole alignment: it eats the leftover width so
     the licence and the button stay welded to the right edge whatever the
     price reads. */
  .card-row .row-price{
    order:2; flex:0 0 auto; margin-right:auto;
    text-align:left; font-size:15px;
  }
  .card-row .row-license{order:3; display:block; flex:0 1 auto; min-width:0;}
  .card-row .row-license:not(:empty){margin-right:8px;}
  .card-row .license-select{width:auto; max-width:150px;}
  .card-row .row-action{order:4; flex:0 0 auto;}
  .card-row .license-btn{width:auto; min-width:84px; padding:9px 14px;}
}
