﻿/* =============================================================================
   ezmuze central — design layer
   =============================================================================

   Loaded AFTER site.css, which is the original Blazor site's stylesheet (1931
   lines of w3-css-style utilities). This file does not replace it: it restyles
   the handful of classes the pages actually use, so every page gets the new
   look without being rewritten one at a time.

   WHERE THE VALUES COME FROM. Not invented — they are ezmuze studio's own
   tokens, from design-guide.md in the studio repo (§1 colour, §2 typography,
   §3 spacing, §4 corner radius, §5 motion). The site fronts the app, so it
   should look like it was designed by whoever designed the app, rather than
   like a different product from a different decade.

   WHAT MADE IT LOOK "METRO", and what each is replaced with:

     body { font-weight: 100 }        hairline type at every size
       -> 400 body / 600 headings, a real weight hierarchy

     rgba(200,200,200,.7) slabs       full-bleed light-grey and black bands,
     rgba(0,0,0,.7)                   alternating, no edges, no depth
       -> the app's dark surface family, 1px bordered, blurred over the video

     padding: 128px                   fixed slabs that overflow a phone
       -> a fluid clamp() scale, and content capped at a readable measure

     ALL-CAPS NAV                     Windows-8 chrome
       -> sentence case, which is design-guide.md §2's app-wide rule

   CORNER RADIUS IS DELIBERATELY 0. design-guide.md §4 locks the app to hard
   edges everywhere except knobs and switches, on the grounds that hard-edged
   chrome is the DAW convention (Ableton, Reaper). Rounding the website would
   have made it look less like the thing it is selling, not more modern. The
   modern feeling here is meant to come from type, spacing, borders, depth and
   motion instead. Easy to revisit: change --radius.
   ========================================================================== */

:root {
  /* -- surfaces: design-guide.md §1.1 dark palette ------------------------ */
  --backdrop:      #0d0d11;   /* SurfaceBackdrop (13,13,17)  */
  --panel:         #16161c;   /* SurfacePanel    (22,22,28)  */
  --header:        #1e1e26;   /* SurfaceHeader   (30,30,38)  */
  --raised:        #282833;   /* SurfaceRaised   (40,40,51)  */
  --border:        #3c3c4a;   /* SurfaceBorder   (60,60,74)  */

  /* Panels are translucent and blurred rather than opaque, so the backdrop
     behind them is visible through the glass. That used to be a video; it is
     now a painted gradient, and these stay translucent because the depth they
     give is the point, not what happens to be underneath. */
  --panel-a:       rgba(22, 22, 28, 0.82);
  --raised-a:      rgba(40, 40, 51, 0.78);
  --header-a:      rgba(16, 16, 21, 0.86);

  /* -- text -------------------------------------------------------------- */
  --text:          #e9e9f0;
  --text-dim:      #a6a6b6;
  --text-faint:    #7c7c8c;

  /* -- accents ----------------------------------------------------------- */
  --brand:         #ff00d8;   /* .ezmuzePurple, and the logo's magenta       */
  --brand-soft:    #ff78a8;   /* AccentModNegative — brand pink, easier on   */
  --green:         #60e0a0;   /* AccentModPositive — the logo's green        */
  --blue:          #96c8ff;   /* AccentSelection — focus rings, links        */
  --amber:         #ffc440;   /* AccentLiveAutomation                        */

  --positive:      #3a6c4a;   --positive-hover: #4a8a5e;  --positive-press: #60a874;
  --neutral:       #34343c;   --neutral-hover:  #42424c;  --neutral-press:  #50505c;
  --negative:      #783a3a;   --negative-hover: #9e4848;

  /* -- geometry: §3 base grid is 12px, §4 hard edges --------------------- */
  --u: 12px;
  --radius: 0px;
  --measure: 68ch;            /* readable line length for body copy          */
  --page: 1220px;             /* content cap; the backdrop still runs edge to edge */
  --tracklist: 820px;         /* a list of tracks reads at this, not at --page  */

  /* -- motion: §5, 100-150ms, ease-out in ------------------------------- */
  --t: 140ms;
  --ease: cubic-bezier(0.2, 0.7, 0.3, 1);
}

/* =============================================================== typography */

/* site.css sets font-weight:100 on body AND every heading, which is where most
   of the thin, washed-out feeling came from. */
body, h1, h2, h3, h4, h5, h6, .button, input, select, textarea, button {
  font-family: system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
  font-weight: 400;
  font-style: normal;
}

body, html {
  line-height: 1.55;                 /* was 1.8 — airy to the point of loose */
  color: var(--text);
  background: var(--backdrop);
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
}

h1, h2, h3, h4, h5 {
  font-weight: 600;
  line-height: 1.15;
  letter-spacing: -0.015em;
  margin: 0 0 calc(var(--u) * 0.75);
  color: var(--text);
}

/* Fluid, so a phone gets a proportionate headline rather than a desktop one
   that wraps four times. */
h1 { font-size: clamp(28px, 3.4vw, 42px); }
h2 { font-size: clamp(22px, 2.6vw, 30px); }
h3 { font-size: clamp(19px, 1.9vw, 23px); letter-spacing: -0.01em; }
h4 { font-size: clamp(16px, 1.5vw, 18px); font-weight: 500; color: var(--text-dim); }

p  { margin: 0 0 var(--u); max-width: var(--measure); color: var(--text-dim); }
p:last-child { margin-bottom: 0; }

b, strong { font-weight: 600; }   /* colour inherits: see hero buttons */
hr { border: 0; border-top: 1px solid var(--border); margin: calc(var(--u) * 2.5) 0; }

.jumbo {
  font-size: clamp(44px, 8.5vw, 84px) !important;
  font-weight: 300;
  letter-spacing: -0.035em;
  line-height: 1.02;
  display: inline-block;
}
.xxlarge { font-size: clamp(32px, 7vw, 44px) !important; font-weight: 300; letter-spacing: -0.03em; }

.ezmuzePurple { color: var(--brand); }

a { color: var(--blue); text-decoration: none; transition: color var(--t) var(--ease); }
a:hover { color: #c2ddff; text-decoration: underline; text-underline-offset: 3px; }

/* ================================================================= surfaces */

/* The three alternating band classes. All three are now dark: the light-grey
   band was the single most dated element on the site, and losing it costs
   nothing because the alternation survives as panel-vs-raised. */
.light-greyalpha, .light-grayalpha,
.hover-light-greyalpha:hover, .hover-light-grayalpha:hover {
  background-color: var(--raised-a) !important;
  color: var(--text) !important;
}

.light-blackalpha, .hover-light-blackalpha:hover {
  background-color: var(--panel-a) !important;
  color: var(--text) !important;
}

/* Kept tinted, because the brand hue earning a place on the page is the point
   of having one — but at a fraction of rgba(100,0,200,.6). */
.light-purplealpha, .hover-light-purplealpha:hover,
.lighter-purplealpha, .hover-lighter-purplealpha:hover {
  background-color: rgba(58, 22, 74, 0.72) !important;
  color: var(--text) !important;
}

/* THESE PANELS ARE NOT BLURRED, and the note above about glass is why they no
   longer need to be. They were, at blur(16px) saturate(115%), and it cost 60%
   of the frame budget on every page for an effect nobody could see.

   WHAT MADE IT INVISIBLE was the backdrop becoming a painted gradient. When
   this rule was written the thing behind these panels was a video, and blurring
   a moving image through translucent glass is the whole trick. A gradient is
   already smooth, so blurring it returns the same pixels -- the panels were
   paying for a filter over something with nothing to filter.

   WHAT IT COST. These panels are full height, so the browser re-blurred a
   region the size of the page on every frame the backdrop moved behind it.
   Measured on the live site: the front page scrolled at a median 29ms per frame
   with 99% of frames over 20ms, and at a locked 16.7ms with this one
   declaration gone. /vault and /history were the same, 34fps and 31fps to 60.
   Nothing else on the page moved the number -- not the shadows, not the 3D
   cases, not the background images.

   The translucency stays: --panel-a and friends are still rgba, so the backdrop
   is still visible through them. It is only the blur that has gone, and the
   before and after screenshots are indistinguishable. */
.container, .slimcontainer, .light-greyalpha, .light-blackalpha, .light-purplealpha {
  border-top: 1px solid var(--border);
  border-bottom: 1px solid var(--border);
}

/* Fluid padding, and a cap on measure so body copy does not run the full width
   of a 27" display. The video still fills the viewport behind it. */
.container, .slimcontainer {
  width: auto !important;
  max-width: none;
  padding: clamp(20px, 4.5vw, 60px) clamp(16px, 5vw, 64px) !important;
  margin-bottom: 4px;
  box-sizing: border-box;
}

.container > .row,
.container > .row-padding,
.slimcontainer > .row-padding,
.slimcontainer > .row {
  max-width: var(--page);
  margin-inline: auto;
}

/* ================================================================== buttons */

/* No !important on the colours: several pages still pass an inline
   backgroundColor for primary/destructive intent, and an !important here would
   flatten all of them into one grey. Shape, motion and focus are safe to force;
   colour is not. */
.button, .btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 8px;
  padding: 10px 18px;
  min-height: 42px;                  /* comfortably tappable */
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background-color: var(--neutral);
  color: var(--text);
  font-size: 15px;
  line-height: 1.2;
  cursor: pointer;
  user-select: none;
  transition: background-color var(--t) var(--ease),
              border-color var(--t) var(--ease),
              color var(--t) var(--ease),
              transform var(--t) var(--ease);
}

.button:hover, .btn:hover {
  background-color: var(--neutral-hover);
  border-color: #55556a;
  color: var(--text) !important;     /* site.css forces #000 on hover */
  text-decoration: none;
}
.button:active, .btn:active { transform: translateY(1px); background-color: var(--neutral-press); }

.button:focus-visible, .btn:focus-visible,
a:focus-visible, input:focus-visible, select:focus-visible, textarea:focus-visible {
  outline: 2px solid var(--blue);
  outline-offset: 2px;
}

/* Intent classes, so pages can stop hand-colouring buttons inline. */
.btn--primary  { background-color: var(--positive); border-color: #4d7f61; color: #eafff2; }
.btn--primary:hover  { background-color: var(--positive-hover); border-color: #61a179; }
.btn--primary:active { background-color: var(--positive-press); }

.btn--ghost { background-color: rgba(255,255,255,0.04); }
.btn--ghost:hover { background-color: rgba(255,255,255,0.09); }

.btn--danger { background-color: var(--negative); border-color: #8f4b4b; color: #ffecec; }
.btn--danger:hover { background-color: var(--negative-hover) !important; }

/* =================================================================== navbar */

/* NOT overflow: hidden any more.
   site.css:507 hid the bar's overflow, which cut the top and bottom off
   anything taller than the bar -- which is exactly what the avatar is.

   The clearfix replaces what that hiding also did: contain floated children.
   Inside .top the bar is laid out with flex and the floats are turned off, so
   there it is belt and braces; it matters for any .bar outside .top, which is
   where the rule it is replacing came from. */
.bar { overflow: visible !important; }
.bar::after { content: ""; display: table; clear: both; }

.bar {
  background: var(--header-a) !important;
  -webkit-backdrop-filter: blur(18px) saturate(120%);
  backdrop-filter: blur(18px) saturate(120%);
  border-bottom: 1px solid var(--border);
  box-shadow: 0 1px 24px rgba(0, 0, 0, 0.45);
}

.bar .bar-item.button {
  min-height: 0;
  border: 0;
  background: transparent;
  color: var(--text-dim);
  font-size: 14px;
  padding: 10px 14px;
  border-radius: var(--radius);
}
/* THE GREEN IS THE POINT, and the !important is what makes it stick. site.css
   line 1389 says `.button:hover { color: #000 !important; background-color:
   #ccc !important }`, and this rule set its background WITHOUT !important -- so
   the nav lost that half and kept its own colour, leaving white text on a solid
   #ccc slab. It read as the bar flashing white, and it was very nearly
   illegible.

   Green rather than a fixed-up grey because the site already has a hover
   language and this was the one place not speaking it: .btn--ghost:hover is a
   green wash with a green rim and a green icon, and so is the ring on your own
   face in the corner. A nav item is lighter than a button, so this is the same
   idea at a lower volume -- a wash, a hairline and a soft throw, not a slab. */
.bar .bar-item.button:hover,
.bar .bar-item.button:focus-visible {
  background-color: rgba(96, 224, 160, 0.13) !important;
  color: #eafff2 !important;
  box-shadow: inset 0 0 0 1px rgba(96, 224, 160, 0.30),
              0 8px 22px -14px rgba(96, 224, 160, 0.9);
}
/* The icon carries the accent, exactly as it does on .btn--ghost. */
.bar .bar-item.button:hover i,
.bar .bar-item.button:focus-visible i { color: var(--green); }

/* The wordmark. */
.bar .bar-item-black {
  background: transparent !important;
  color: var(--text) !important;
  letter-spacing: 0.34em;
  font-size: 15px;
  border: 0;
}

/* site.css swaps the desktop links for a burger at its own 601px breakpoint,
   which leaves 600-900px rendering a row of links with no room for them. */
@media (max-width: 900px) {
  .bar .right.hide-small { display: none !important; }
  .bar .fakeClick.hide-large.hide-medium { display: inline-block !important; }
  .bar .bar-item-player { display: none !important; }
}

/* ==================================================================== forms */

input[type="text"], input[type="password"], input[type="email"],
input[type="date"], input[type="number"], select, textarea, .input {
  width: 100%;
  box-sizing: border-box;
  padding: 11px 13px;
  min-height: 42px;
  background: rgba(0, 0, 0, 0.4);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  color: var(--text);
  font-size: 15px;
  transition: border-color var(--t) var(--ease), background-color var(--t) var(--ease);
}
input:hover, select:hover, textarea:hover { border-color: #4d4d5f; }
input:focus, select:focus, textarea:focus { border-color: var(--blue); background: rgba(0, 0, 0, 0.55); }
::placeholder { color: var(--text-faint); }

/* =================================================================== tables */

table.table, table.table-dark {
  width: 100%;
  border-collapse: collapse;
  background: rgba(0, 0, 0, 0.35) !important;
  border: 1px solid var(--border);
}
table.table th, table.table-dark th {
  text-align: left;
  font-weight: 600;
  font-size: 13px;
  letter-spacing: 0.04em;
  text-transform: none;
  color: var(--text-dim);
  background: var(--header-a);
  border-bottom: 1px solid var(--border);
  padding: 10px 12px;
}
table.table td, table.table-dark td {
  padding: 10px 12px;
  border-bottom: 1px solid rgba(60, 60, 74, 0.5);
}
table.table tbody tr:hover td { background: rgba(255, 255, 255, 0.035); }

/* A data table cannot reflow, so on a phone it scrolls inside its own box
   instead of pushing the whole page sideways. */
.table-scroll { overflow-x: auto; -webkit-overflow-scrolling: touch; }

/* ================================================================= sections */

/* The home page's feature rows. Were layout <table>s, which cannot stack —
   on a phone they squeezed a screenshot and a paragraph into two columns of
   nothing. Grid, so they become one column when there is no room for two. */
.feature {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, 1.05fr);
  gap: clamp(20px, 3.5vw, 48px);
  align-items: center;
  max-width: var(--page);
  margin: 0 auto calc(var(--u) * 3);
}
.feature:last-child { margin-bottom: 0; }
.feature__media img {
  display: block;
  width: 100%;
  height: auto;
  border: 1px solid var(--border);
  background: var(--backdrop);
}
.feature--flip .feature__media { order: -1; }

@media (max-width: 860px) {
  .feature { grid-template-columns: minmax(0, 1fr); gap: var(--u); }
  .feature--flip .feature__media { order: 0; }
  .feature__media { margin-top: 4px; }
}

/* A row of actions that wraps instead of overflowing. */
.actions { display: flex; flex-wrap: wrap; gap: 10px; align-items: center; }

/* ============================================================ housekeeping */

img, video { max-width: 100%; }

/* site.css floats .col; without this the page can scroll sideways on a phone. */
html, body { overflow-x: hidden; }

/* THE BACKDROP, WHICH WAS A 3.4MB VIDEO.
   1.mp4 autoplayed on every page of the site -- the single heaviest thing it
   served, downloaded before anyone had decided to stay, and on a phone's data
   as readily as on a desktop. It was then dimmed to 62% brightness and covered
   by translucent panels, so most of those megabytes arrived to be hidden.

   This is what is left when you keep the part that was doing the work: colour
   and depth behind the glass. Three very soft pools in the brand hues over the
   backdrop, fixed so they do not scroll, and costing nothing to send because
   they are four lines of CSS rather than a file.

   It is on a pseudo-element rather than on body so that the gradients can be
   fixed while the page scrolls normally over them. */
body::before {
  content: "";
  position: fixed;
  inset: 0;
  z-index: -100;
  pointer-events: none;
  /* The palette's own three accents -- brand magenta, the logo's green and
     the selection blue -- at the opacity the video sat at once it had been
     dimmed. Any stronger and the panel text starts fighting it. */
  background:
    radial-gradient(120vw 90vh at 10% -12%, rgba(255, 0, 216, 0.13), transparent 62%),
    radial-gradient(100vw 80vh at 94% 6%,  rgba(150, 200, 255, 0.09), transparent 60%),
    radial-gradient(130vw 100vh at 52% 112%, rgba(96, 224, 160, 0.09), transparent 66%);
}

@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after { transition-duration: 1ms !important; animation-duration: 1ms !important; }
}

/* ============================================================= mobile menu */

/* The old sheet was <h2> links with hand-placed margins, absolutely positioned
   at top:64 with a hardcoded white background — and two of its entries pointed
   at routes that do not exist. This is a real sheet: full width, scrollable if
   the list outgrows the screen, and tappable at 48px a row. */
.mobile-menu {
  position: fixed;
  top: 64px;
  left: 0;
  right: 0;
  max-height: calc(100vh - 64px);
  overflow-y: auto;
  z-index: 10000;
  background: var(--header-a);
  -webkit-backdrop-filter: blur(18px) saturate(120%);
  backdrop-filter: blur(18px) saturate(120%);
  border-bottom: 1px solid var(--border);
  box-shadow: 0 18px 40px rgba(0, 0, 0, 0.5);
  padding: 8px 0 12px;
}

.mobile-menu__item {
  display: flex;
  align-items: center;
  gap: 14px;
  padding: 14px clamp(16px, 5vw, 28px);
  min-height: 48px;
  font-size: 17px;
  color: var(--text);
  cursor: pointer;
  border-bottom: 1px solid rgba(60, 60, 74, 0.45);
}
.mobile-menu__item:last-child { border-bottom: 0; }
/* The same green as the bar above. These are the same navigation on a smaller
   screen, and a phone answering a hover in a different colour would be the
   sort of difference nobody decides on, only inherits. */
.mobile-menu__item:hover, .mobile-menu__item:active {
  background-color: rgba(96, 224, 160, 0.13) !important;
  color: #eafff2;
  text-decoration: none;
}
.mobile-menu__item i { width: 22px; text-align: center; color: var(--text-faint); }
.mobile-menu__item:hover i, .mobile-menu__item:active i { color: var(--green); }

/* ===================================================================== hero */

/* Two columns on a desktop, one on a phone. The old hero left the right-hand
   half of a widescreen empty: copy capped at a readable measure and nothing
   beside it. The browser screenshot goes there, which also lets the page prove
   its own headline -- "make music in your browser" next to a picture of exactly
   that. */
.hero {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, 1.08fr);
  gap: clamp(24px, 4vw, 56px);
  align-items: center;
  max-width: var(--page);
  margin: 0 auto;
}

.hero__copy .jumbo { margin-bottom: 2px; }

.hero__badge {
  display: flex;
  align-items: center;
  gap: 6px;
  font-size: 14px;
  color: var(--text-dim);
  margin-bottom: calc(var(--u) * 1.5);
}
.hero__badge b { color: var(--brand-soft); letter-spacing: 0.02em; }
/* Sitting on the download buttons rather than under the lockup: it needs a
   little air above it and none of the paragraph spacing below. */
.hero__badge--above { margin: var(--u) 0 6px; }

/* The page's real headline, so it outranks the section h1s below it. */
.hero__tagline {
  font-size: clamp(24px, 3vw, 34px) !important;
  font-weight: 500;
  margin-bottom: var(--u);
}

.hero__fineprint { font-size: 14px; color: var(--text-faint); margin-top: var(--u); }
.hero__fineprint a { color: var(--brand-soft); }
.hero__fineprint a:hover { color: #ffa8c8; }

/* The hero's case, where the hero leads with the product rather than with a
   screenshot. Centred in its column like the history covers, and outside the
   img rules below: a case is not a picture in a frame, it draws its own
   edges and casts its own shadow. */
.hero__media--case { display: flex; justify-content: center; }

.hero__media img {
  display: block;
  width: 100%;
  height: auto;
  border: 1px solid var(--border);
  box-shadow: 0 24px 60px rgba(0, 0, 0, 0.55);
}

@media (max-width: 900px) {
  .hero { grid-template-columns: minmax(0, 1fr); gap: var(--u); }
  .hero__media { margin-top: 8px; }
}

.button__note { color: var(--text-faint); font-weight: 400; }

/* A line under the download row about what the operating system is about to do
   -- currently macOS refusing an un-notarized build. Fineprint sized, because
   it is a footnote to the buttons and not a warning about the software; sitting
   under them rather than beside one, because it applies to a platform rather
   than to a file. Not .hero__fineprint: the same block is on the store and the
   beta page, neither of which has a hero. */
.studiodl__firstrun {
  font-size: 13px;
  color: var(--text-faint);
  margin: 10px 0 0;
}
.studiodl__firstrun i { margin-right: 6px; }

/* The download row is secondary to the green browser CTA above it. */
.actions .btn--ghost { font-size: 14px; }

/* Breathing room between the hero and the section that follows it inside the
   same container -- they were running together with only a paragraph's margin. */
.hero + .row { margin-top: calc(var(--u) * 4); }
.hero + .row > h1 { margin-top: 0; }

/* A history entry: prose beside its cover. The cover is art, not a screenshot,
   so it gets a column of its own rather than half the width.

   The case sits in the MIDDLE of the entry, not at the top of it. Entries run
   to wildly different lengths -- some four paragraphs, some one -- and with the
   cover pinned to the top a long entry left it stranded against three hundred
   pixels of empty column. Centred, the art reads as the partner of the prose at
   any length. */
/* The cover column grows with the window. It was a flat 280px, so every pixel
   a wide display added went to the prose, and the case -- the one thing on the
   page that is worth looking at large -- stayed the size it is on a laptop.
   The cap stops the measure beside it dropping below a readable line length. */
.feature--history { grid-template-columns: minmax(0, 1fr) minmax(0, clamp(260px, 30vw, 420px)); align-items: center; }
.feature__media--case { display: flex; justify-content: center; }
.feature__media--case .dvd { max-width: 100%; }

@media (max-width: 860px) {
  .feature--history { grid-template-columns: minmax(0, 1fr); }
  /* The cover leads on a phone: it is the most recognisable thing about an
     entry, and scrolling past four paragraphs to reach it is backwards. */
  .feature--history .feature__media { order: -1; margin-bottom: 8px; }
}

/* ============================================================== the lockup */

/* The mark plus the wordmark, as the app draws it.
   - the mark is public/img/ezmuze-mark.svg, generated from LogoMark.cs's own 26
     control points, so the site and the app draw the same curve
   - "ezmuze" is Comfortaa Bold and "studio" Comfortaa Light, which is what
     GustUI's Theme.WordmarkFont / WordmarkLightFont name
   - "studio" is the app's brand magenta (249,0,183), not the site's older
     #FF00D8 -- the lockup should match the product, not the stylesheet */
.lockup { display: flex; align-items: center; gap: clamp(12px, 1.6vw, 22px); }

.lockup__mark {
  width: clamp(54px, 7vw, 86px);
  height: auto;
  flex: 0 0 auto;
}

.lockup__word {
  font-family: Comfortaa, system-ui, sans-serif;
  font-size: clamp(38px, 7vw, 72px);
  line-height: 1;
  letter-spacing: -0.005em;
  white-space: nowrap;
}
.lockup__word b { font-weight: 700; color: var(--text); }
.lockup__word span { font-weight: 300; color: #f900b7; }

/* ============================================================== previews */

/* A song you can hear that is not in the vault: the studio demos on the home
   page, a pack's demo in the store. A ROW THAT PLAYS, not a browser control
   strip -- the audio itself is handed to the player at the bottom of the page,
   so this only has to be the thing you press. See components/Previews.tsx. */
.previews { display: grid; gap: 10px; max-width: 720px; }
.preview {
  display: flex;
  align-items: center;
  gap: 12px;
  width: 100%;
  text-align: left;
  border: 1px solid var(--border);
  background: rgba(0, 0, 0, 0.35);
  padding: 10px 14px 10px 12px;
  color: inherit;
  font: inherit;
  cursor: pointer;
  transition: background 120ms;
}
.preview:hover { background: rgba(255, 255, 255, 0.05); }
.preview__cover { flex: none; display: block; object-fit: cover; border: 1px solid var(--border); }
.preview__text { display: flex; flex-direction: column; gap: 2px; min-width: 0; flex: 1; }
.preview__text b { font-size: 15px; }
.preview__text span { font-size: 13px; color: var(--text-faint); }

/* The play button reads as one without being a nested <button>: the whole row
   is the control, and a button inside a button is invalid and unreachable by
   keyboard in the way it looks like it should be. */
.preview__play {
  flex: none;
  width: 34px; height: 34px;
  display: grid; place-items: center;
  border-radius: 50%;
  background: var(--green);
  color: #04140c;
  font-size: 13px;
}
.preview:hover .preview__play { filter: brightness(1.1); }
.preview.is-playing .preview__play { box-shadow: 0 0 14px rgba(96, 224, 160, 0.6); }

/* ============================================================ pack notice */

/* The "your packs still work" panel. It is the single most reassuring thing on
   the page for anyone who spent money on ezmuze before, so it gets a surface of
   its own rather than being a sentence in a paragraph. */
.keepsake {
  border: 1px solid var(--border);
  border-left: 3px solid var(--green);
  background: rgba(96, 224, 160, 0.05);
  padding: clamp(16px, 2.4vw, 26px);
  max-width: var(--page);
  margin: 0 auto calc(var(--u) * 2);
}
.keepsake h3 { margin-bottom: 8px; }
.keepsake p { max-width: none; }

/* ================================================================ out now */

/* The release flag under the lockup. A plain word on a green tint rather than a
   banner: it is the first thing the eye lands on after the name, and that is
   all it needs to be. The dot pulses slowly -- design-guide.md §5 keeps motion
   for things that are live, and "out now" is exactly that. */
.outnow {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  margin: calc(var(--u) * 1.25) 0 calc(var(--u) * 0.75);
  padding: 5px 12px 5px 10px;
  border: 1px solid rgba(96, 224, 160, 0.45);
  background: rgba(96, 224, 160, 0.10);
  color: var(--green);
  font-size: 15px;
  font-weight: 600;
  letter-spacing: 0.04em;
}
.outnow__dot {
  width: 8px;
  height: 8px;
  border-radius: 50%;
  background: var(--green);
  box-shadow: 0 0 0 0 rgba(96, 224, 160, 0.6);
  animation: outnow-pulse 2.4s ease-out infinite;
}
@keyframes outnow-pulse {
  0%   { box-shadow: 0 0 0 0 rgba(96, 224, 160, 0.55); }
  70%  { box-shadow: 0 0 0 8px rgba(96, 224, 160, 0); }
  100% { box-shadow: 0 0 0 0 rgba(96, 224, 160, 0); }
}
@media (prefers-reduced-motion: reduce) { .outnow__dot { animation: none; } }

/* "And there is more": the features that do not need a screenshot, as a grid
   of short cards. Three across on a desktop, two on a tablet, one on a phone. */
.morelist {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: var(--u);
  max-width: var(--page);
  margin: var(--u) auto calc(var(--u) * 2);
}
.morelist__item {
  border: 1px solid var(--border);
  background: var(--panel-a);
  padding: clamp(14px, 2vw, 20px);
}
.morelist__item h3 { margin-bottom: 6px; font-size: 18px; }
.morelist__item h3 i { color: var(--green); margin-right: 6px; width: 20px; text-align: center; }
.morelist__item p { max-width: none; margin-bottom: 0; color: var(--text-dim); }
@media (max-width: 960px) { .morelist { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
@media (max-width: 600px) { .morelist { grid-template-columns: minmax(0, 1fr); } }

/* "Beta versions" under the front page's downloads: a native <details>, so it
   opens with the keyboard and with no script, styled as a quiet row with a
   chevron that turns. Closed by default -- most people want the release. */
.betadl {
  margin-top: 14px;
  border: 1px solid var(--border);
  background: rgba(255, 255, 255, 0.02);
}
.betadl > summary {
  list-style: none;
  cursor: pointer;
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 10px 14px;
  min-height: 42px;
  font-size: 14px;
  font-weight: 600;
  color: var(--text-dim);
}
.betadl > summary::-webkit-details-marker { display: none; }
.betadl > summary::before {
  content: '';
  width: 7px;
  height: 7px;
  border-right: 2px solid currentColor;
  border-bottom: 2px solid currentColor;
  transform: rotate(-45deg);
  transition: transform 150ms ease-out;
}
.betadl[open] > summary::before { transform: rotate(45deg); }
.betadl > summary:hover { color: var(--text); background: rgba(255, 255, 255, 0.04); }
.betadl > summary:focus-visible { outline: 2px solid var(--green); outline-offset: -2px; }
.betadl__body { padding: 0 14px 14px; }
.betadl__body > p { font-size: 14px; color: var(--text-dim); margin: 2px 0 10px; }
/* The buttons in here are the hero's, one size quieter: same height as the
   release row's, not the taller default a details element's text gives them. */
.betadl__body .button { box-sizing: border-box; min-height: 42px; padding: 10px 18px; font-size: 14px; }
.betadl__body > .studiodl__firstrun { font-size: 13px; color: var(--text-faint); margin: 10px 0 0; }
@media (prefers-reduced-motion: reduce) { .betadl > summary::before { transition: none; } }

/* The second set of buttons, at the bottom of the home page. */
.getit {
  border: 1px solid var(--border);
  border-left: 3px solid var(--brand-soft);
  background: rgba(255, 120, 168, 0.05);
  padding: clamp(16px, 2.4vw, 26px);
  max-width: var(--page);
  margin: calc(var(--u) * 3) auto calc(var(--u) * 2);
}
.getit h1 { margin-top: 0; }

/* ================================================================== store */

/* The store's categories, as the site's own tab strip.
   .tabs centres itself in a --page column because it is normally a page's own
   strip; here it is inside the store's container, which has already done that,
   so the width and the top margin are handed back. */
.tabs--store { max-width: none; margin: 0 0 12px; }
.tabs--store .tabs__tab { text-decoration: none; }
.tabs--store .tabs__tab:hover { text-decoration: none; }

/* The store's head sits in the SAME column as the shelf under it -- the --page
   cap plus the same fluid inset every .mgrid, .pgrid and .rowlist uses. It used
   to be a .slimcontainer, whose inset is computed differently, so the toolbar
   and the tiles below it started sixteen pixels apart; and whose 60px of
   vertical padding left the toolbar adrift in an empty slab once the six
   category buttons moved out of the row and became a tab strip. */
.storehead {
  max-width: var(--page);
  margin: 0 auto;
  padding: clamp(14px, 2.2vw, 26px) clamp(16px, 5vw, 64px) 0;
}

.storebar {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 10px;
  margin-bottom: 14px;
}
.storebar__sort { display: flex; flex-wrap: wrap; gap: 8px; }
.storebar__sort { margin-left: auto; }

/* A fixed basis is the whole point: the search field must not be sized by the
   result list it is filtering. */
.storebar__search { flex: 0 1 300px; min-width: 200px; }
.storebar__search .searchbar { position: relative; margin: 0; }
.storebar__search .search_input {
  width: 100%;
  padding-right: 40px;
  margin: 0;
}
.storebar__search .search_icon {
  position: absolute;
  right: 0; top: 0; bottom: 0;
  width: 38px;
  display: flex; align-items: center; justify-content: center;
  color: var(--text-faint);
}
.storebar__search .search_icon:hover { color: var(--text); text-decoration: none; }

/* The loop-pack shelf's one filter. It sits with the search rather than over
   the shelf, because it is part of the same question -- which of these am I
   looking at -- and a banner above the tiles would read as a warning about the
   packs that ARE shown. */
.storebar__filter {
  display: flex;
  flex-direction: column;
  gap: 2px;
  font-size: 0.9em;
}
.storefilter { display: flex; align-items: center; gap: 6px; cursor: pointer; }
.storefilter input { margin: 0; cursor: pointer; }
.storefilter__note { color: var(--text-faint); font-size: 0.85em; }

/* Said quietly. It is a fact about the pack, not a warning about it. */
.pdp__unvalidated {
  color: var(--text-faint);
  font-size: 0.9em;
  margin: 4px 0 0;
}
.pdp__unvalidated i { margin-right: 6px; }

@media (max-width: 700px) {
  .storebar__sort { margin-left: 0; width: 100%; }
  .storebar__search { flex: 1 1 100%; }
  .storebar__filter { flex: 1 1 100%; }
}

/* ============================================== the legacy search bar, undone */

/* site.css made this an expanding search: the input is width:0 with a
   TRANSPARENT CARET, and only `.searchbar:hover > .search_input` gives it
   250px and a visible caret. So it collapsed the moment the pointer left,
   you could not see what you were typing, and the width animated under the
   cursor. That is the "barmy" behaviour, and it was never the flex layout.
   Specificity has to beat `.searchbar:hover > .search_input` (0,3,0). */
.storebar__search .searchbar,
.storebar__search .searchbar:hover {
  height: auto;
  background: none;
  border-radius: 0;
  padding: 0;
}
.storebar__search .searchbar > .search_input,
.storebar__search .searchbar:hover > .search_input {
  width: 100%;
  padding: 11px 40px 11px 13px;
  line-height: 1.2;
  caret-color: var(--text);
  color: var(--text);
  background: rgba(0, 0, 0, 0.4);
  border: 1px solid var(--border);
  transition: border-color var(--t) var(--ease), background-color var(--t) var(--ease);
}
.storebar__search .searchbar:hover > .search_icon { background: none; color: var(--text); }

/* ==================================================================== vault */

.vault {
  display: grid;
  grid-template-columns: minmax(0, 5fr) minmax(0, 7fr);
  gap: clamp(16px, 2.5vw, 32px);
  max-width: var(--page);
  margin: 0 auto;
}
@media (max-width: 820px) { .vault { grid-template-columns: minmax(0, 1fr); } }

.vault__pane {
  border: 1px solid var(--border);
  background: rgba(0, 0, 0, 0.28);
  padding: clamp(12px, 1.6vw, 18px);
  min-width: 0;
}
.vault__head { display: flex; align-items: baseline; gap: 10px; margin-bottom: 10px; }
.vault__head h3 { margin: 0; }
.vault__count {
  font-size: 12px;
  color: var(--text-faint);
  border: 1px solid var(--border);
  padding: 1px 7px;
}
.vault__pane .input { margin-bottom: 10px; }

/* Rows: design-guide.md §9.5 makes list rows a universal spec in the app --
   one height, hover, a selected state. These were bare <p> tags. */
.rowlist { list-style: none; margin: 0; padding: 0; }
.rowlist__row {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 9px 10px;
  min-height: 40px;
  color: var(--text);
  border-bottom: 1px solid rgba(60, 60, 74, 0.45);
  cursor: pointer;
  transition: background-color var(--t) var(--ease), color var(--t) var(--ease);
}
.rowlist li:last-child .rowlist__row { border-bottom: 0; }
.rowlist__row:hover { background: rgba(255, 255, 255, 0.06); text-decoration: none; }
.rowlist__row.is-selected {
  background: rgba(150, 200, 255, 0.10);
  box-shadow: inset 2px 0 0 var(--blue);
  color: #fff;
}
.rowlist__row--play i { color: var(--text-faint); font-size: 11px; width: 14px; }
.rowlist__row--play:hover i, .rowlist__row--play.is-selected i { color: var(--green); }
.rowlist__row span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.rowlist__empty { color: var(--text-faint); padding: 10px; }
/* A line under a heading saying what the list below it is. */
.rowlist__note { color: var(--text-dim); margin: -4px 0 14px; max-width: var(--measure); }

/* =========================================================== the user menu ===
   Your own face, and everything about your account behind it. Bigger than the
   bar it sits on, which is the point -- it is the one thing up there that is
   yours, and hanging below the edge makes it read as a face rather than as
   another button. Nothing else on the bar is allowed to be taller, so nothing
   else needed the bar to stop clipping. */
/* OUT OF THE FLOW, which is the only way it can be taller than the bar. In
   flow it is a floated child like every other item, so containing the floats --
   which is what stops the bar collapsing -- also stretches the bar to fit it,
   and 42px became 54 with nothing hanging over anything. Absolute, and the row
   beside it reserves the width so the two do not overlap.

   It hangs DOWNWARD only: the bar is at the top of the window, so an overhang
   split evenly would put the top of the face off the screen. */
.bar { position: relative; }
#myNavbar .right.hide-small { padding-right: 66px; }

.usermenu__face {
  position: absolute;
  right: 10px;
  top: 3px;
  /* ABOVE THE LINE UNDER THE BAR. .top::after draws that hairline, and a
     pseudo-element paints after its element's content -- so with both of them
     positioned and neither given a z-index, tree order decided, and the line
     went straight across the face. */
  z-index: 2;
  appearance: none;
  border: 0;
  background: transparent;
  padding: 0;
  line-height: 0;
  cursor: pointer;
  border-radius: 50%;
  box-shadow: 0 0 0 2px var(--header), 0 6px 18px -6px rgba(0, 0, 0, 0.8);
  transition: box-shadow var(--t) var(--ease), transform var(--t) var(--ease);
}
.usermenu__face:hover { transform: translateY(-1px); }
.usermenu__face:hover,
.usermenu__face.is-on {
  box-shadow: 0 0 0 2px var(--green), 0 6px 18px -6px rgba(0, 0, 0, 0.8);
}
.usermenu__face .avatar { display: block; }

.usermenu__panel { min-width: 240px; }

/* Who you are and what you have: facts, not menu items. */
.usermenu__who {
  display: flex; flex-direction: column; gap: 2px;
  padding: 10px 12px 8px;
}
.usermenu__name { color: var(--text); font-size: 15px; }
.usermenu__tag { color: var(--text-faint); font-size: 12px; }
.usermenu__tag .fa-xbox { color: var(--green); margin-right: 4px; }

.usermenu__sigma .usermenu__balance {
  margin-left: auto;
  font-variant-numeric: tabular-nums;
  color: var(--green);
}

/* ================================================= making a record ===
   The artist's side of an album. The tiles are the playlist grid, because a
   record and a playlist are the same shape on screen -- a picture, a name and
   a count -- and giving them two looks would say they were different things. */
.albums { margin-bottom: clamp(20px, 3vw, 30px); }
.albums .plnew { margin-top: 12px; }
.pltile.is-open .pltile__name { color: var(--green); }

.albumedit {
  margin-top: 16px;
  padding: 14px;
  border: 1px solid var(--border);
  background: rgba(0, 0, 0, 0.25);
}
.albumedit__head {
  display: flex; align-items: center; gap: 12px; flex-wrap: wrap;
  margin-bottom: 10px;
}
.albumedit__head h4 { margin: 0; flex: 1 1 auto; }
.albumedit__acts { display: flex; gap: 8px; flex-wrap: wrap; }

/* The position on the record, which is what the numeric prefix means inside an
   album folder. Tabular so a two-digit track does not shift the names. */
.filerow__num {
  min-width: 20px;
  color: var(--text-faint); font-size: 12px;
  font-variant-numeric: tabular-nums; text-align: right;
}

.albumadd {
  margin-top: 14px; padding-top: 12px;
  border-top: 1px solid rgba(255, 255, 255, 0.08);
}
.albumadd h5 { margin: 0 0 8px; color: var(--text-dim); font-size: 13px; font-weight: 500; }

/* ======================================================= the vault search ===
   One box for the whole vault, with the two badges inside it. They are inside
   because they are not a second control -- they say what the box is looking at,
   and a filter sitting somewhere else would be a second thing to notice. */
.vsearch { position: relative; max-width: var(--page); margin: 0 auto clamp(20px, 3vw, 32px); }

.vsearch__bar { position: relative; display: flex; align-items: center; }
.vsearch__icon {
  position: absolute; left: 14px;
  color: var(--text-faint); font-size: 14px; pointer-events: none;
}
.vsearch__input {
  width: 100%;
  padding: 14px 96px 14px 40px;      /* room for the icon, and for both badges */
  font-size: 15px;
}

.vsearch__kinds { position: absolute; right: 8px; display: flex; gap: 4px; }
.vsearch__kind {
  appearance: none; border: 0; background: transparent;
  padding: 6px; line-height: 0; cursor: pointer;
  border-radius: var(--radius);
  transition: opacity var(--t) var(--ease), filter var(--t) var(--ease);
  /* OFF is dark, not gone. The control has to say what it would do if you
     pressed it again, and an empty space says nothing. */
  opacity: 0.32;
  filter: grayscale(1);
}
.vsearch__kind:hover { opacity: 0.6; }
.vsearch__kind.is-on { opacity: 1; filter: none; }
.vsearch__kind.is-on:hover { background: rgba(255, 255, 255, 0.07); }
.vsearch__kind img { width: 22px; height: 22px; display: block; }

/* The results. Absolute rather than a portal: this sits at the top of the page
   with nothing above it that clips, and it has to match the box's own width,
   which a panel placed from a rect would not. */
.vsearch__panel {
  position: absolute;
  left: 0; right: 0; top: calc(100% + 6px);
  z-index: 60;
  max-height: min(60vh, 520px);
  overflow-y: auto;
  padding: 6px;
  border: 1px solid var(--border);
  background: #14151a;
  box-shadow: 0 24px 60px rgba(0, 0, 0, 0.66);
}
.vsearch__none { padding: 14px; color: var(--text-faint); font-size: 14px; }
.vsearch__group + .vsearch__group { border-top: 1px solid rgba(255, 255, 255, 0.07); margin-top: 4px; padding-top: 4px; }
.vsearch__label {
  padding: 8px 10px 4px;
  font-size: 11px; letter-spacing: 0.12em; text-transform: uppercase;
  color: var(--text-faint);
}
.vsearch__hit {
  display: grid;
  grid-template-columns: 34px minmax(0, 1fr) auto;
  align-items: center;
  gap: 12px;
  width: 100%;
  padding: 7px 10px;
  border: 0; background: transparent; color: var(--text);
  text-align: left; cursor: pointer;
  font: inherit;
}
/* Hover and keyboard are the SAME state. Two different highlights moving
   independently is how you end up pressing enter on the wrong row. */
.vsearch__hit.is-at { background: rgba(255, 255, 255, 0.07); }
.vsearch__art {
  width: 34px; height: 34px; overflow: hidden;
  display: grid; place-items: center;
  background: var(--panel); color: var(--text-faint);
}
.vsearch__art img { width: 100%; height: 100%; object-fit: cover; display: block; }
.vsearch__name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.vsearch__sub { color: var(--text-faint); font-size: 12px; white-space: nowrap; }

@media (max-width: 620px) { .vsearch__sub { display: none; } }

/* ============================================================== albums ===
   A record is a subfolder of an artist's, so an album card is a shelf card
   that happens to open a folder rather than a track. It is a <button> and not
   a link because it does not go anywhere: it filters the page you are on, and
   says so by putting ?album= in the address. */
.albumcard {
  appearance: none;
  border: 0;
  background: transparent;
  padding: 0;
  text-align: left;
  cursor: pointer;
}
/* A record's slot holds a jewel case rather than a flat square, so it cannot
   clip: the case turns and grows, and .tcard__art's overflow: hidden cuts it
   back to a 168px square -- which is what took the corners off a hovered case
   and sliced the top off the art. No panel and no shadow behind it either: the
   case is the object and brings its own, and a rounded square around a box in
   perspective reads as a frame it is trapped in.
   BOTH CLASSES IN THE SELECTOR, and that is the whole fix. This rule and
   .tcard__art are each one class, so specificity ties and source order decides
   -- and .tcard__art is two thousand lines further down the file, so it won
   every one of these declarations. Compounding them puts this at 0,2,0 and
   settles it wherever either rule happens to live. */
.tcard__art.albumcard__art {
  display: grid;
  place-items: center;
  overflow: visible;
  border-radius: 0;
  background: transparent;
  box-shadow: none;
}
.albumcard:hover .tcard__name { color: var(--text); }

/* Which record you are in. The artist's own header stays above it, because you
   have not left them -- this is a place inside their page, not another page. */
.albumhead {
  display: flex;
  align-items: center;
  gap: clamp(12px, 2vw, 20px);
  max-width: var(--tracklist);
  margin: 0 auto clamp(16px, 2.5vw, 24px);
  padding: 12px;
  border: 1px solid var(--border);
  background: rgba(0, 0, 0, 0.25);
}
/* The case at the head of a record, beside its name. flex: none so a long
   album title cannot squeeze it. */
.albumhead__case { flex: none; }
.albumhead h2 { margin: 0 0 2px; }
.albumhead h2::after { display: none; }
.albumhead__facts { color: var(--text-faint); font-size: 13px; margin: 0 0 10px; }

/* A folder-wide action under its heading -- emptying the trash. */
.cloudgroup__act { display: flex; gap: 8px; align-items: center; margin: 0 0 14px; }


.pager { display: flex; align-items: center; gap: 10px; margin-top: 12px; }
.pager span { font-size: 13px; color: var(--text-faint); font-variant-numeric: tabular-nums; }
.pager .button { min-height: 34px; padding: 6px 12px; }

/* ============================================================== store rows */

/* The compact list rows and the big cards both sat on the old alternating
   alpha slabs with no edge between them. */
.slimcontainer.light-purplealpha + .slimcontainer,
.storerow {
  border: 1px solid var(--border);
}

/* =============================================================================
   NEON PASS
   =============================================================================
   Taking the cues from the ezmuze studio box art rather than from the app's
   chrome: near-black ground, halftone dots, the green-to-magenta disc, colour
   coded sequencer rows, a spectrum curve running red through blue to green,
   and glowing wires over the top of it.

   This deliberately overrides the restraint above -- flat surfaces, hard
   edges, borders doing all the work. That read as tidy rather than as ezmuze.
   ========================================================================== */

:root {
  --neon-green:   #27f836;
  --neon-magenta: #ff05c7;
  --neon-cyan:    #5ad9ff;
  --neon-violet:  #a06bff;
  --grad-brand:   linear-gradient(120deg, var(--neon-green) 0%, var(--neon-cyan) 42%, var(--neon-magenta) 100%);
  --radius: 6px;                     /* the flat-0 decision is retired */
}

/* -- the ground: halftone, as on the art ---------------------------------- */
body::before {
  content: "";
  position: fixed;
  inset: 0;
  pointer-events: none;
  z-index: 0;
  /* Faint, and fading from the top rather than concentrated there: at 0.055
     with a tight mask the only place the dots were NOT covered by a panel was
     the gap under the nav, so they read as a dotted band rather than as
     texture. */
  background-image: radial-gradient(rgba(255, 255, 255, 0.032) 1px, transparent 1px);
  background-size: 5px 5px;
  mask-image: linear-gradient(180deg, #000 0%, rgba(0, 0, 0, 0.55) 45%, transparent 100%);
  -webkit-mask-image: linear-gradient(180deg, #000 0%, rgba(0, 0, 0, 0.55) 45%, transparent 100%);
}
.top, .container, .slimcontainer, .mobile-menu { position: relative; z-index: 1; }

/* -- headings in the wordmark's own typeface ------------------------------ */
h1, h2, .jumbo, .xxlarge, .hero__tagline {
  font-family: Comfortaa, system-ui, sans-serif;
  letter-spacing: -0.02em;
}
h1 { font-weight: 700; }

/* A lit rule under the big section headings, the way the spectrum runs across
   the art. */
.row > h1, .container > h1 { position: relative; padding-bottom: 12px; }
.row > h1::after, .container > h1::after {
  content: "";
  position: absolute;
  left: 0; bottom: 0;
  width: 116px; height: 2px;
  background: var(--grad-brand);
  border-radius: 2px;
  box-shadow: 0 0 14px rgba(255, 5, 199, 0.55);
}

hr {
  height: 1px;
  border: 0;
  background: linear-gradient(90deg, transparent, var(--border) 12%, var(--border) 88%, transparent);
}

/* -- surfaces get an edge that catches the light -------------------------- */
.container, .slimcontainer {
  border-top: 1px solid rgba(255, 255, 255, 0.07);
  border-bottom: 1px solid rgba(0, 0, 0, 0.5);
}

.feature__media img,
.hero__media img,
.vault__pane,
.preview,
.keepsake {
  border-radius: var(--radius);
}

/* The screenshots are the best thing on the page: let them glow a little, and
   lift on hover the way the art's panels sit above their own reflections. */
.feature__media img, .hero__media img {
  box-shadow: 0 18px 44px rgba(0, 0, 0, 0.6),
              0 0 0 1px rgba(255, 255, 255, 0.06),
              0 0 34px -12px rgba(90, 217, 255, 0.5);
  transition: transform var(--t) var(--ease), box-shadow var(--t) var(--ease);
}
.feature__media img:hover, .hero__media img:hover {
  transform: translateY(-3px);
  box-shadow: 0 24px 56px rgba(0, 0, 0, 0.65),
              0 0 0 1px rgba(255, 255, 255, 0.10),
              0 0 44px -8px rgba(255, 5, 199, 0.45);
}

/* -- the mark, lit --------------------------------------------------------- */
.lockup__mark {
  filter: drop-shadow(0 0 26px rgba(39, 248, 54, 0.35))
          drop-shadow(0 0 34px rgba(255, 5, 199, 0.30));
}

/* -- buttons ---------------------------------------------------------------- */
.button, .btn { border-radius: var(--radius); }

.btn--primary {
  background: linear-gradient(135deg, #2f7d52, #1f6a63);
  border-color: rgba(39, 248, 54, 0.45);
  color: #eafff2;
  box-shadow: 0 0 0 1px rgba(39, 248, 54, 0.10), 0 8px 24px -10px rgba(39, 248, 54, 0.55);
}
.btn--primary:hover {
  background: linear-gradient(135deg, #3a9a64, #268076);
  border-color: rgba(39, 248, 54, 0.75);
  box-shadow: 0 0 0 1px rgba(39, 248, 54, 0.18), 0 12px 30px -10px rgba(39, 248, 54, 0.7);
}

/* WHY !important IS HERE, AND ONLY HERE.

   site.css line 1389 is

       .button:hover { color: #000 !important; background-color: #ccc !important }

   -- the Blazor site's idea of a hover, applied to every button anywhere.
   !important beats source order, so nothing in this file could get past it.

   The green primary button escaped by accident: its colour is a gradient,
   which is a background-IMAGE, and the image paints over the grey the old
   rule forces underneath. A ghost button's colour is a background-COLOR,
   which is the exact property being overridden -- so it went solid #ccc on
   hover, which is what "washed out and goes white" was.

   The declarations below are !important so they win, rather than the whole
   legacy rule being neutralised: turning it off would change every button
   on the site at once, including ones nothing here has ever styled.

   GREEN, not the cyan it used to hint at. Cyan is --blue, the selection
   colour, and it belongs to focus rings and to what is currently chosen.
   These buttons DO something, and the site's colour for that is the green
   on the play button sitting next to them. */
.btn--ghost {
  /* Was 4.5% white with a 14% border: legible, but only just, and it read
     as disabled next to a filled button. */
  background: rgba(255, 255, 255, 0.07);
  border-color: rgba(255, 255, 255, 0.22);
}
.btn--ghost:hover {
  background-color: rgba(96, 224, 160, 0.15) !important;
  border-color: rgba(96, 224, 160, 0.6);
  color: #eafff2 !important;
  box-shadow: 0 0 0 1px rgba(96, 224, 160, 0.14),
              0 10px 26px -12px rgba(96, 224, 160, 0.8);
}
.btn--ghost:hover i { color: var(--green); }

/* Not a button -- it is the "already in your cloud" state, and it says so.
   Without this the old rule greys it out under the pointer, which reads as
   something you can press. */
.button.is-done:hover {
  background-color: var(--positive) !important;
  color: #fff !important;
}

/* -- nav ------------------------------------------------------------------- */
.bar { border-bottom: 1px solid rgba(255, 255, 255, 0.08); }
.bar::after {
  content: "";
  position: absolute;
  left: 0; right: 0; bottom: -1px;
  height: 1px;
  background: var(--grad-brand);
  opacity: 0.5;
}
.top .bar { position: relative; }
/* The hover lives with the other button states, near the top of this file --
   it used to be half here, which is how its background and its colour ended up
   being decided by two different stylesheets. */

/* -- the packs panel, lit down its edge ------------------------------------ */
.keepsake {
  border-left: 0;
  background:
    linear-gradient(90deg, rgba(39, 248, 54, 0.09), rgba(255, 5, 199, 0.05) 42%, transparent 78%),
    rgba(0, 0, 0, 0.32);
  position: relative;
  overflow: hidden;
}
.keepsake::before {
  content: "";
  position: absolute;
  left: 0; top: 0; bottom: 0;
  width: 3px;
  background: var(--grad-brand);
  box-shadow: 0 0 18px rgba(39, 248, 54, 0.55);
}

/* -- previews, like the art's channel rows -------------------------------- */
.preview { position: relative; overflow: hidden; background: rgba(0, 0, 0, 0.34); }
.preview::before {
  content: "";
  position: absolute;
  left: 0; top: 0; bottom: 0;
  width: 3px;
  background: var(--neon-cyan);
  box-shadow: 0 0 16px rgba(90, 217, 255, 0.7);
}
.preview:nth-child(2)::before { background: var(--neon-magenta); box-shadow: 0 0 16px rgba(255, 5, 199, 0.7); }
.preview__text b { font-family: Comfortaa, system-ui, sans-serif; font-weight: 700; }

/* -- vault rows: a lit edge on the selected one --------------------------- */
.rowlist__row.is-selected {
  background: linear-gradient(90deg, rgba(90, 217, 255, 0.14), transparent 62%);
  box-shadow: inset 3px 0 0 var(--neon-cyan);
}
.rowlist__row--play:hover i, .rowlist__row--play.is-selected i {
  color: var(--neon-green);
  filter: drop-shadow(0 0 6px rgba(39, 248, 54, 0.8));
}

.vault__pane, .preview, .keepsake { border-color: rgba(255, 255, 255, 0.09); }

/* -- the brand pink, everywhere it was the older flat magenta -------------- */
.ezmuzePurple { color: var(--neon-magenta); text-shadow: 0 0 22px rgba(255, 5, 199, 0.45); }

/* ============================================================ store cards */

.pgrid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(392px, 1fr));
  gap: clamp(12px, 1.6vw, 20px);
  max-width: var(--page);
  margin: 0 auto clamp(24px, 4vw, 56px);
  padding-inline: clamp(16px, 5vw, 64px);
}
@media (max-width: 420px) { .pgrid { grid-template-columns: minmax(0, 1fr); } }

.pcard {
  display: grid;
  grid-template-columns: auto minmax(0, 1fr) auto;
  gap: 16px;
  align-items: start;
  padding: 16px;
  border: 1px solid rgba(255, 255, 255, 0.09);
  border-radius: var(--radius);
  background: linear-gradient(150deg, rgba(58, 22, 74, 0.5), rgba(0, 0, 0, 0.4) 55%);
  color: var(--text);
  cursor: pointer;
  transition: transform var(--t) var(--ease), box-shadow var(--t) var(--ease),
              border-color var(--t) var(--ease);
}
.pcard:hover {
  transform: translateY(-2px);
  border-color: rgba(255, 5, 199, 0.45);
  box-shadow: 0 16px 34px -14px rgba(0, 0, 0, 0.8), 0 0 30px -14px rgba(255, 5, 199, 0.8);
  text-decoration: none;
}
.pcard__art { flex: 0 0 auto; }
.pcard__name {
  font-family: Comfortaa, system-ui, sans-serif;
  font-size: 19px;
  margin: 0 0 2px;
}
.pcard__by { font-size: 12px; color: var(--text-faint); margin-bottom: 8px; }
.pcard__pitch {
  font-size: 14px;
  color: var(--text-dim);
  margin: 0;
  max-width: none;
  display: -webkit-box;
  -webkit-line-clamp: 3;
  -webkit-box-orient: vertical;
  overflow: hidden;
}
/* The card's right-hand column: price at the top, preview at the bottom, both
   against the right edge. It stretches to the card's full height so "bottom"
   means the bottom of the card rather than the bottom of the price pill. */
.pcard__side {
  align-self: stretch;
  display: flex;
  flex-direction: column;
  align-items: flex-end;
  justify-content: space-between;
  gap: 10px;
}

.pcard__price {
  font-family: Comfortaa, system-ui, sans-serif;
  font-weight: 700;
  font-size: 15px;
  white-space: nowrap;
  padding: 4px 10px;
  border: 1px solid rgba(255, 255, 255, 0.16);
  border-radius: 999px;
}
.pcard__price.is-free {
  color: var(--neon-green);
  border-color: rgba(39, 248, 54, 0.45);
  box-shadow: 0 0 18px -8px rgba(39, 248, 54, 0.9);
}

/* The toolbar band was a full-bleed purple slab of its own. */
.slimcontainer.light-purplealpha:has(.storebar) {
  background: rgba(0, 0, 0, 0.35) !important;
  margin-bottom: clamp(16px, 2.5vw, 28px);
}

/* =============================================================== the player */

.playerbar {
  position: fixed;
  left: 0; right: 0; bottom: 0;
  z-index: 9000;
  background: rgba(12, 12, 16, 0.92);
  -webkit-backdrop-filter: blur(22px) saturate(130%);
  backdrop-filter: blur(22px) saturate(130%);
  border-top: 1px solid rgba(255, 255, 255, 0.10);
  box-shadow: 0 -14px 40px rgba(0, 0, 0, 0.55);
  padding-bottom: env(safe-area-inset-bottom);
}
/* The lit hairline the rest of the design uses for "this is ezmuze". */
.playerbar::before {
  content: "";
  position: absolute; left: 0; right: 0; top: -1px; height: 1px;
  background: var(--grad-brand);
  opacity: 0.75;
}

.playerbar__inner {
  display: grid;
  /* The meta column carries a cover, two lines of linked text and the stars
     now, so it needs a floor -- at 1fr against a 2fr seek bar the artist name
     was clipped to "madninjaskill:". */
  grid-template-columns: minmax(260px, 1.2fr) auto minmax(160px, 1.6fr) auto;
  align-items: center;
  gap: clamp(10px, 2vw, 26px);
  max-width: var(--page);
  margin: 0 auto;
  padding: 10px clamp(12px, 4vw, 28px);
}

.playerbar__meta { display: flex; align-items: center; gap: 12px; min-width: 0; }
.playerbar__text { min-width: 0; }
.playerbar__track {
  font-weight: 600; font-size: 14px; color: var(--text);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.playerbar__artist {
  font-size: 12px; color: var(--text-faint);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}

/* Four bars that dance while it plays and rest when it does not. */
.playerbar__eq { display: flex; align-items: flex-end; gap: 2px; height: 20px; flex: 0 0 auto; }
.playerbar__eq span {
  width: 3px; height: 4px; border-radius: 1px;
  background: var(--neon-green);
  box-shadow: 0 0 8px rgba(39, 248, 54, 0.8);
}
.playerbar__eq[data-on="true"] span { animation: eq 900ms ease-in-out infinite; }
.playerbar__eq span:nth-child(2) { animation-delay: 140ms; background: var(--neon-cyan);    box-shadow: 0 0 8px rgba(90, 217, 255, 0.8); }
.playerbar__eq span:nth-child(3) { animation-delay: 280ms; background: var(--neon-violet);  box-shadow: 0 0 8px rgba(160, 107, 255, 0.8); }
.playerbar__eq span:nth-child(4) { animation-delay: 420ms; background: var(--neon-magenta); box-shadow: 0 0 8px rgba(255, 5, 199, 0.8); }
@keyframes eq { 0%, 100% { height: 4px } 50% { height: 18px } }

.playerbar__controls { display: flex; align-items: center; gap: 6px; }
.playerbar__btn {
  display: inline-flex; align-items: center; justify-content: center;
  width: 36px; height: 36px;
  border: 0; border-radius: 50%;
  background: transparent; color: var(--text-dim);
  cursor: pointer;
  transition: color var(--t) var(--ease), background-color var(--t) var(--ease),
              transform var(--t) var(--ease);
}
.playerbar__btn:hover:not(:disabled) { color: #fff; background: rgba(255, 255, 255, 0.09); }
.playerbar__btn:disabled { opacity: 0.3; cursor: default; }
.playerbar__btn--play {
  width: 44px; height: 44px;
  color: #06210f;
  background: linear-gradient(135deg, var(--neon-green), #12c9a0);
  box-shadow: 0 0 24px -6px rgba(39, 248, 54, 0.8);
}
.playerbar__btn--play:hover:not(:disabled) {
  color: #06210f;
  background: linear-gradient(135deg, #56ff62, #21e3b6);
  transform: scale(1.05);
}

.playerbar__seek { display: flex; align-items: center; gap: 10px; min-width: 0; }

/* Where you are in the queue, on the button that opens it.
   It used to sit after the artist's name, where "Railblade . 5 of 118" read as
   though the number were part of who made the track. */
.playerbar__queuebtn { display: inline-flex; align-items: center; gap: 6px; }
.playerbar__queuebtn.is-on { color: var(--green); }
.playerbar__pos { font-size: 0.78rem; color: var(--text-dim); font-variant-numeric: tabular-nums; }
.playerbar__queuebtn.is-on .playerbar__pos { color: inherit; }
/* The handle on the drawer: quieter than the list icon and the count, which
   are what you are actually reading. */
.playerbar__queuebtn .fa-chevron-up,
.playerbar__queuebtn .fa-chevron-down { font-size: 0.7em; opacity: 0.75; }

/* The queue, expanding UPWARDS out of the bar.
   Above rather than over: the player is anchored to the bottom of the window,
   and a panel that covered the page would hide the list you were browsing when
   you opened it. */
.playerqueue {
  position: absolute;
  left: 0; right: 0; bottom: 100%;
  max-height: min(52vh, 460px);
  display: flex;
  flex-direction: column;
  background: rgba(12, 12, 16, 0.97);
  border-top: 1px solid rgba(255, 255, 255, 0.10);
  box-shadow: 0 -14px 40px rgba(0, 0, 0, 0.55);
}
.playerqueue__head {
  display: flex; align-items: center;
  padding: 8px 14px;
  color: var(--text-dim);
  font-size: 0.82rem;
  text-transform: uppercase;
  letter-spacing: 0.08em;
  border-bottom: 1px solid rgba(255, 255, 255, 0.07);
}
.playerqueue__list { list-style: none; margin: 0; padding: 4px 0; overflow-y: auto; }
.playerqueue__row > button {
  display: grid;
  grid-template-columns: 28px 32px minmax(0, 1fr) minmax(0, 0.6fr);
  align-items: center;
  gap: 10px;
  width: 100%;
  padding: 5px 14px;
  background: transparent;
  border: 0;
  color: var(--text);
  font: inherit;
  text-align: left;
  cursor: pointer;
}
.playerqueue__row > button:hover { background: rgba(255, 255, 255, 0.06); }
.playerqueue__row.is-current > button { background: rgba(90, 217, 255, 0.10); }
.playerqueue__row.is-current .playerqueue__n { color: var(--neon-cyan, var(--blue)); }
.playerqueue__n { color: var(--text-faint); font-size: 0.8rem; text-align: center; font-variant-numeric: tabular-nums; }
.playerqueue__name, .playerqueue__by { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.playerqueue__by { color: var(--text-dim); font-size: 0.85rem; }

/* The scrubber, drawn as the track's own waveform.
   Taller than a bar because that is the point -- at 4px it would be a bar with
   texture. 34px is enough to read the shape of a song without the player
   growing into the page. */
.waveform {
  position: relative;
  flex: 1 1 auto;
  min-width: 0;
  height: 34px;
  cursor: pointer;
  overflow: hidden;
}
.waveform__canvas { display: block; width: 100%; height: 100%; }

/* Where you would land. A line rather than a tooltip: the waveform already
   says what is there, so the only missing information is "here". */
.waveform__hover {
  position: absolute;
  top: 0; bottom: 0;
  width: 1px;
  background: rgba(255, 255, 255, 0.55);
  pointer-events: none;
}

/* No peaks -- an undecodable file, or one still being computed. A plain bar,
   so the control never vanishes and the track stays scrubbable. */
.waveform.is-plain { height: 6px; background: rgba(255, 255, 255, 0.12); }
.waveform__plain { position: absolute; inset: 0 auto 0 0; background: var(--brand); }
.playerbar__time {
  font-size: 11px; color: var(--text-faint);
  font-variant-numeric: tabular-nums; flex: 0 0 auto;
}

/* One range style for both seek and volume, filled to --pct. */
.playerbar input[type="range"] {
  -webkit-appearance: none; appearance: none;
  width: 100%; height: 4px; padding: 0; min-height: 0;
  border: 0; border-radius: 999px;
  background: linear-gradient(90deg, var(--neon-cyan) var(--pct, 0%), rgba(255, 255, 255, 0.16) var(--pct, 0%));
  cursor: pointer;
}
.playerbar input[type="range"]::-webkit-slider-thumb {
  -webkit-appearance: none; appearance: none;
  width: 12px; height: 12px; border-radius: 50%;
  background: #fff; box-shadow: 0 0 10px rgba(90, 217, 255, 0.9);
}
.playerbar input[type="range"]::-moz-range-thumb {
  width: 12px; height: 12px; border: 0; border-radius: 50%;
  background: #fff; box-shadow: 0 0 10px rgba(90, 217, 255, 0.9);
}

.playerbar__right { display: flex; align-items: center; gap: 8px; }
.playerbar__vol { width: 84px; }
.playerbar__vol-icon { color: var(--text-faint); font-size: 13px; width: 16px; text-align: center; }

/* On a phone: title and transport on one row, seek under it, and the volume
   slider dropped -- the hardware buttons already do that job. */
@media (max-width: 760px) {
  .playerbar__inner {
    grid-template-columns: minmax(0, 1fr) auto auto;
    grid-template-areas: "meta controls close" "seek seek seek";
    gap: 6px 10px;
  }
  .playerbar__meta { grid-area: meta; }
  .playerbar__controls { grid-area: controls; }
  .playerbar__seek { grid-area: seek; }
  .playerbar__right { grid-area: close; }
  .playerbar__vol, .playerbar__vol-icon { display: none; }
}

/* Nothing hides behind the bar. */
body:has(.playerbar) { padding-bottom: 96px; }
@media (max-width: 760px) { body:has(.playerbar) { padding-bottom: 128px; } }

/* ============================================================ vault, again */

/* Both panes scroll instead of paging: 809 artists behind 81 pages of a single
   prev/next pair was the worst interaction on the site. */
.vault__scroll {
  max-height: min(58vh, 560px);
  overflow-y: auto;
  overscroll-behavior: contain;
  border-top: 1px solid rgba(60, 60, 74, 0.5);
  margin-top: 8px;
}
.vault__scroll::-webkit-scrollbar { width: 10px; }
.vault__scroll::-webkit-scrollbar-thumb {
  background: rgba(255, 255, 255, 0.14);
  border-radius: 999px;
  border: 3px solid transparent;
  background-clip: padding-box;
}
.vault__scroll::-webkit-scrollbar-thumb:hover {
  background: rgba(255, 255, 255, 0.26);
  background-clip: padding-box;
}

.vault__head { flex-wrap: wrap; }
.vault__playall { margin-left: auto; min-height: 32px; padding: 6px 12px; font-size: 13px; }

/* A-Z jump. Letters with nobody under them stay put but dim, so the strip does
   not reflow while you filter. */
.azbar {
  /* 27 equal columns rather than flex-wrap: as a flex row the letters wrapped
     into a ragged second line whose items then stretched to fill it. */
  display: grid;
  grid-template-columns: repeat(27, minmax(0, 1fr));
  gap: 1px;
  margin: 8px 0 0;
}
.azbar__c {
  min-width: 0;
  padding: 3px 0;
  font-size: 11px;
  border: 0; border-radius: 3px;
  background: transparent; color: var(--text-dim);
  cursor: pointer;
  transition: background-color var(--t) var(--ease), color var(--t) var(--ease);
}
.azbar__c:hover:not(:disabled) { background: rgba(90, 217, 255, 0.18); color: #fff; }
.azbar__c:disabled { color: rgba(255, 255, 255, 0.14); cursor: default; }

.rowlist__icon { width: 16px; text-align: center; color: var(--text-faint); font-size: 11px; flex: 0 0 auto; }
.rowlist__row--play:hover .rowlist__icon { color: var(--neon-green); }

/* The same dancing bars, smaller, on whichever row is playing. */
.eq { display: inline-flex; align-items: flex-end; gap: 2px; height: 12px; }
.eq span {
  width: 2px; height: 3px; border-radius: 1px;
  background: var(--neon-green);
  animation: eq 900ms ease-in-out infinite;
}
.eq span:nth-child(2) { animation-delay: 150ms; }
.eq span:nth-child(3) { animation-delay: 300ms; }

@media (prefers-reduced-motion: reduce) {
  .playerbar__eq span, .eq span { animation: none !important; height: 10px; }
}

/* ========================================================= vault, spotified */

.vaulthead {
  display: flex;
  align-items: flex-end;
  justify-content: space-between;
  gap: 20px;
  flex-wrap: wrap;
  max-width: var(--page);
  margin: 0 auto clamp(18px, 3vw, 30px);
}
.vaulthead p { margin: 0; }
.vaulthead__search { max-width: 320px; margin: 0; }

.artistgrid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(132px, 1fr));
  gap: clamp(10px, 1.6vw, 20px);
  max-width: var(--page);
  margin: 0 auto;
}

.artisttile {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 10px;
  padding: 14px 8px;
  border: 1px solid transparent;
  border-radius: 10px;
  background: transparent;
  color: var(--text);
  cursor: pointer;
  transition: background-color var(--t) var(--ease), transform var(--t) var(--ease),
              border-color var(--t) var(--ease);
}
.artisttile:hover {
  background: rgba(255, 255, 255, 0.05);
  border-color: rgba(255, 255, 255, 0.09);
  transform: translateY(-2px);
}
.artisttile:hover .avatar {
  box-shadow: 0 10px 26px -8px rgba(0, 0, 0, 0.85), 0 0 26px -8px rgba(90, 217, 255, 0.75);
}
.artisttile__name {
  font-size: 13px;
  text-align: center;
  width: 100%;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* -- the avatar itself ---------------------------------------------------- */
.avatar {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  border-radius: 50%;
  overflow: hidden;
  flex: 0 0 auto;
  color: rgba(255, 255, 255, 0.92);
  font-family: Comfortaa, system-ui, sans-serif;
  font-weight: 700;
  letter-spacing: 0.02em;
  box-shadow: 0 6px 18px -8px rgba(0, 0, 0, 0.8);
  transition: box-shadow var(--t) var(--ease);
}
.avatar img { width: 100%; height: 100%; object-fit: cover; display: block; }

/* -- one artist ----------------------------------------------------------- */
.backlink {
  display: inline-flex; align-items: center; gap: 8px;
  background: none; border: 0; padding: 6px 0;
  color: var(--text-dim); font-size: 14px; cursor: pointer;
  transition: color var(--t) var(--ease);
}
.backlink:hover { color: #fff; }

.artisthead {
  display: flex;
  align-items: flex-end;
  gap: clamp(16px, 3vw, 32px);
  max-width: var(--page);
  margin: 8px auto clamp(20px, 3vw, 32px);
  flex-wrap: wrap;
}
/* Takes the width the avatar leaves. Without this it shrank to its own content,
   which meant the actions row had no slack in it -- and the track filter, which
   pushes itself to the far end of that row, had nothing to push against and sat
   flush against the Shuffle button. min-width: 0 so a long name can still
   ellipsis rather than forcing the row wider. */
.artisthead__text { flex: 1 1 320px; min-width: 0; }

.artisthead__kicker {
  font-size: 12px; letter-spacing: 0.14em; text-transform: uppercase;
  color: var(--text-faint); margin-bottom: 4px;
}
.artisthead h1 {
  font-size: clamp(30px, 6vw, 64px);
  line-height: 1.02;
  margin: 0 0 6px;
  padding-bottom: 0;
}
/* The gradient rule under section headings would look odd under a name. */
.artisthead h1::after { display: none; }
.artisthead p { margin: 0 0 14px; }
.artisthead__play { min-width: 116px; }

/* The controls over a track list: search on the left, sort on the right, on one
   line the width of the list they act on. The search box was 340px centred over
   a list running the full 1220, which read as an island floating above
   something else rather than as a control belonging to it. */
.tracktools {
  display: flex;
  align-items: center;
  gap: 10px;
  max-width: var(--tracklist);
  margin: 0 auto 12px;
}
/* Sort with no search beside it -- a playlist, which is not searched. */
.tracktools--right { justify-content: flex-end; }
.tracksearch { flex: 1 1 auto; min-width: 0; margin: 0; display: block; }

/* Two controls, because they answer two questions: what it is ordered by, and
   which way round. The arrow has nothing to reverse under "As listed", and says
   so by being disabled rather than by quietly doing nothing. */
.tracksort { display: flex; gap: 6px; margin-left: auto; }
.tracksort__pick { white-space: nowrap; }
.tracksort__dir { padding-left: 10px; padding-right: 10px; }
.tracksort__dir:disabled { opacity: 0.4; cursor: default; }
.tracksort__dir:disabled:hover {
  background-color: rgba(255, 255, 255, 0.07) !important;
  border-color: rgba(255, 255, 255, 0.22);
  box-shadow: none;
}
/* The chosen row keeps a tick; the others keep its width so the list does not
   jump as you move down it. */
.sortmenu__item.is-on { color: var(--text); }
/* Hidden, not absent: an icon with no glyph has no width, and the labels

   would sit at two different left edges down the same menu. */

.sortmenu__item .is-blank { visibility: hidden; }

/* Filter a track list by where the audio came from.
   Deliberately plain: these are the same tracks either way, and the control
   should read as a lens over one list rather than as a set of tabs between
   two different kinds of thing. */
/* Sits at the far end of the Play/Shuffle row. margin-left:auto is what
   pushes it there -- the row is a flex line, so the filter takes all the
   slack between itself and the buttons that start it. It wraps onto its own
   line when there is no slack left, which on a phone is most of the time. */
.trackfilter { display: flex; gap: 8px; flex-wrap: wrap; margin-left: auto; }
/* On the vault's front page it is a row of its own under the header rather than
   something pushed to the end of a line, so the auto margin has to go. */
.vaulthead__filter { margin: 0 0 clamp(14px, 2vw, 22px); }
.trackfilter__btn { padding: 4px 12px; font-size: 0.8rem; display: inline-flex; align-items: center; gap: 6px; }

/* How a track got here: a 3.0 share, or a 360 upload.
   Inline inside the name rather than a grid column, so the three-column .track
   layout is untouched everywhere it is used. vertical-align keeps it sitting on
   the text baseline instead of dragging the row taller. */
.trackbadge {
  /* Size comes from the width/height ATTRIBUTES, not from here: setting it in
     css would override them and silently ignore the size prop. */
  vertical-align: -0.18em;   /* scales with the text it sits in */
  margin-right: 7px;
  border-radius: 3px;
  flex: 0 0 auto;
}
.trackfilter__btn .trackbadge { margin-right: 0; vertical-align: 0; }

.track__num .trackbadge { margin-right: 0; vertical-align: -0.25em; }

/* On the artwork, bottom right.
   The cover already clips its children, so this cannot escape the tile. The
   ring and shadow are there because these two icons land on art whose colours
   are drawn from the track itself -- one of them will always be a bad
   background for one of them. */
.cover__badge {
  position: absolute;
  right: 5%;
  /* TOP right. Bottom right is where the play button lives -- a 42px circle at
     right 8px / bottom 8px on a card -- so the badge sat underneath it. The
     generated art also runs its title text along the bottom, which makes this
     the free corner twice over. */
  top: 5%;
  border-radius: 3px;   /* softens the square 3.0 tile; nothing to the round one */
  /* drop-shadow, NOT box-shadow: box-shadow traces the element's RECTANGLE, so
     a round transparent logo would wear a square halo. This follows the alpha. */
  filter: drop-shadow(0 0 1px rgba(0, 0, 0, 0.7)) drop-shadow(0 1px 2px rgba(0, 0, 0, 0.5));
  pointer-events: none;
  z-index: 2;
}
.trackfilter__btn.is-on { background: var(--brand); border-color: transparent; color: #fff; }

/* A button that has finished and become a statement. Not disabled -- disabled
   reads as "you cannot", and the truth is "you already did". */
.button.is-done {
  background: var(--positive);
  border-color: transparent;
  color: #fff;
  cursor: default;
}

/* "Move or copy to..." on a cloud song. One row per destination with both
   verbs spelled out, rather than a select plus a separate move/copy toggle:
   the question people actually have is "does my original survive", and the
   answer should be on the button they are about to press. */
.filerow__filing {
  display: flex;
  flex-direction: column;
  gap: 6px;
  padding: 10px 12px;
  background: rgba(0, 0, 0, 0.28);
}
.filerow__filingrow { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
.filerow__filingname { min-width: 92px; opacity: 0.85; font-size: 0.85rem; }

/* -- the track list ------------------------------------------------------- */
/* A TRACK LIST IS NOT A PAGE WIDE. At the full 1220 the name sat on the far
   left and the plays, rating and buttons on the far right, with the better
   part of nine hundred pixels of nothing in between -- a row you had to read
   across rather than take in. There is nothing to put in that gap: a vault
   track has a name, an artist you already know, and its numbers. So the gap
   goes instead, and the rows read as a list. */
.tracklist {
  list-style: none;
  /* CENTRED, like everything above it. .artisthead is a --page block with auto
     margins, so an artist page is a centred column; a list left-aligned to the
     container's own padding sat well to the left of the header on any screen
     wider than --page, which read as the list having come loose. Both are
     centred, so both keep the same centre line at every width. */
  margin: 0 auto;
  padding: 0;
  max-width: var(--tracklist);
}

/* Stands in for the rows a windowed list is not drawing, so the page keeps
   its real height and the scrollbar keeps its real size. */
.tracklist__gap { list-style: none; }
.track {
  display: grid;
  grid-template-columns: 34px minmax(0, 1fr) auto;
  align-items: center;
  gap: 14px;
  width: 100%;
  padding: 9px 12px;
  border: 0;
  border-radius: 6px;
  background: transparent;
  color: var(--text);
  text-align: left;
  cursor: pointer;
  transition: background-color var(--t) var(--ease);
}
.track:hover { background: rgba(255, 255, 255, 0.06); }
.track.is-playing { background: rgba(90, 217, 255, 0.10); color: #fff; }

.track__num {
  display: inline-flex; align-items: center; justify-content: center;
  color: var(--text-faint); font-size: 12px;
  font-variant-numeric: tabular-nums;
}
/* Number normally, play triangle on hover -- the convention every music app
   uses, and it saves a permanent column of icons. */
.track__play { display: none; font-size: 11px; }
.track:hover .track__i { display: none; }
.track:hover .track__play { display: inline; color: var(--neon-green); }
.track.is-playing .track__i { color: var(--neon-cyan); }

.track__name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.track__artist { color: var(--text-faint); font-size: 12px; white-space: nowrap; }
@media (max-width: 620px) { .track__artist { display: none; } }

/* ================================================= plays and star ratings */

.track {
  /* room for the stats column now */
  grid-template-columns: 34px minmax(0, 1fr) auto;
}
.track__stats { display: flex; align-items: center; gap: 14px; }
.track__plays {
  display: inline-flex; align-items: center; gap: 5px;
  font-size: 12px; color: var(--text-faint);
  font-variant-numeric: tabular-nums;
}
.track__plays i { font-size: 9px; }

.stars { display: inline-flex; align-items: center; gap: 1px; }
.stars__s {
  background: none;
  border: 0;
  padding: 0 1px;
  font-size: 12px;
  line-height: 1;
  color: rgba(255, 255, 255, 0.16);
  cursor: pointer;
  transition: color var(--t) var(--ease), transform var(--t) var(--ease);
}
.stars__s.is-on { color: rgba(255, 255, 255, 0.45); }
/* Yours is the brand amber; everyone else's average stays neutral, so the two
   readings are never confused. */
.stars.is-mine .stars__s.is-on { color: var(--amber); filter: drop-shadow(0 0 5px rgba(255, 196, 64, 0.6)); }
.stars:not(.is-readonly) .stars__s:hover { transform: scale(1.22); }
.stars:not(.is-readonly):hover .stars__s.is-on { color: var(--amber); }
.stars.is-readonly .stars__s { cursor: default; }
.stars__count {
  font-size: 11px;
  color: var(--text-faint);
  margin-left: 5px;
  font-variant-numeric: tabular-nums;
}

/* The stars are fiddly at a phone's tap size and the row is the important
   target, so they only appear where there is room to hit them. */
@media (max-width: 620px) {
  .stars { display: none; }
  .track__stats { gap: 8px; }
}

/* ========================================================= gamertag claims */

.claimline { display: flex; align-items: center; gap: 8px; font-size: 14px; margin: 0 0 12px; }
.claimline i { color: var(--neon-green); }
.claimline__you { color: var(--text-faint); }

/* Only ever seen by the twenty-odd people whose username IS their gamertag,
   so it is worth making a moment of. */
.claimbox {
  display: flex; align-items: center; justify-content: space-between;
  gap: 16px; flex-wrap: wrap;
  padding: 14px 16px;
  margin: 0 0 14px;
  border: 1px solid rgba(39, 248, 54, 0.35);
  border-radius: var(--radius);
  background: linear-gradient(120deg, rgba(39, 248, 54, 0.10), rgba(255, 5, 199, 0.05) 70%, transparent);
  box-shadow: 0 0 30px -14px rgba(39, 248, 54, 0.8);
}
.claimbox p { margin: 2px 0 0; font-size: 13px; }

/* ========================================================== claiming a tag */

.claimpanel {
  max-width: var(--page);
  margin: 0 auto;
  padding: clamp(16px, 2.4vw, 26px);
  border: 1px solid rgba(255, 255, 255, 0.10);
  border-radius: var(--radius);
  background:
    linear-gradient(120deg, rgba(39, 248, 54, 0.07), rgba(90, 217, 255, 0.04) 55%, transparent),
    rgba(0, 0, 0, 0.32);
}
.claimpanel h3 { display: flex; align-items: center; gap: 10px; margin-bottom: 8px; }
.claimpanel h3 i { color: var(--neon-green); }
.claimpanel.is-done { background: rgba(0, 0, 0, 0.3); }
.claimpanel p { max-width: 70ch; }

.claimpanel__row { display: flex; gap: 10px; flex-wrap: wrap; margin-top: 12px; }
.claimpanel__row .input { max-width: 260px; margin: 0; }

/* The code is the whole point of the panel, so it is the loudest thing in it. */
.claimcode {
  display: flex; align-items: center; gap: 12px;
  margin: 14px 0;
  padding: 12px 16px;
  border: 1px solid rgba(39, 248, 54, 0.4);
  border-radius: var(--radius);
  background: rgba(0, 0, 0, 0.45);
  width: fit-content;
}
.claimcode code {
  font-family: ui-monospace, "Cascadia Mono", Consolas, monospace;
  font-size: clamp(20px, 3vw, 28px);
  letter-spacing: 0.14em;
  color: var(--neon-green);
  text-shadow: 0 0 18px rgba(39, 248, 54, 0.55);
}

.claimsteps { margin: 0 0 12px; padding-left: 20px; color: var(--text-dim); font-size: 14px; }
.claimsteps li { margin-bottom: 5px; }

.claimpanel__hint  { font-size: 13px; color: var(--text-faint); margin-top: 12px; }
.claimpanel__error { font-size: 14px; color: #ff9a9a; margin-top: 12px; }
.claimpanel__warn  {
  font-size: 13px; color: var(--amber);
  display: flex; align-items: flex-start; gap: 8px; margin-top: 12px;
}

/* ======================================================= profile and vault */

/* A claimed artist is shown under their ezmuze name; the tick says the person
   behind it proved the gamertag is theirs. */
.artisttile { position: relative; }
.artisttile__claimed {
  position: absolute;
  top: 12px; right: 14px;
  font-size: 12px;
  color: var(--neon-green);
  filter: drop-shadow(0 0 6px rgba(39, 248, 54, 0.8));
}

.artisthead__gamertag {
  display: inline-flex; align-items: center; gap: 8px;
  font-size: 13px; color: var(--text-faint);
  margin: 0 0 10px;
}
.artisthead__gamertag i { color: var(--neon-green); }

.artisthead__bio {
  max-width: 70ch;
  margin: 0 0 12px;
  color: var(--text-dim);
  white-space: pre-wrap;      /* keep the line breaks people type */
}

.claimline__hint { color: var(--text-faint); font-size: 13px; }
.claimline__hint i { color: var(--text-faint); }

/* ------------------------------------------------------------ edit panel */

.editprofile {
  max-width: var(--page);
  margin: 0 auto 16px;
  padding: clamp(16px, 2.4vw, 24px);
  border: 1px solid rgba(255, 255, 255, 0.10);
  border-radius: var(--radius);
  background: rgba(0, 0, 0, 0.32);
}
.editprofile h3 { margin-bottom: 14px; }

.editprofile__pic {
  display: flex; align-items: center; gap: 18px;
  flex-wrap: wrap;
  margin-bottom: 18px;
}
.editprofile__hint { font-size: 12px; color: var(--text-faint); margin: 8px 0 0; }
.editprofile__error { font-size: 14px; color: #ff9a9a; margin-top: 12px; }

.editprofile__bio { display: block; position: relative; margin-bottom: 14px; }
.editprofile__bio > span:first-child {
  display: block; font-size: 12px; opacity: 0.75; margin-bottom: 4px;
}
.editprofile__bio textarea {
  width: 100%; min-height: 96px; resize: vertical;
  font-family: inherit; font-size: 14px; line-height: 1.5;
}
.editprofile__count {
  position: absolute; right: 10px; bottom: 8px;
  font-size: 11px; color: var(--text-faint);
  font-variant-numeric: tabular-nums;
  pointer-events: none;
}

/* ================================================== shared cloud songs */

.artisttile__meta { font-size: 11px; color: var(--text-faint); }

.sharedlist { max-width: var(--page); margin: clamp(20px, 3vw, 32px) auto 0; }
.sharedlist h3 { margin-bottom: 4px; }
.sharedlist__note { font-size: 13px; color: var(--text-faint); margin: 0 0 10px; max-width: 70ch; }

/* A project, not a recording: no hover-to-play, no pointer, and the row says
   what it is rather than offering a play button that would do nothing. */
.track.is-notplayable { cursor: default; opacity: 0.82; }
.track.is-notplayable:hover { background: transparent; }
.track.is-notplayable .track__num i { color: var(--neon-violet); font-size: 12px; }
.track.is-notplayable .track__plays {
  font-size: 11px;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--text-faint);
  border: 1px solid rgba(255, 255, 255, 0.14);
  border-radius: 999px;
  padding: 2px 8px;
}

/* ============================================== profile tabs and file lists */

.tabs {
  display: flex;
  gap: 4px;
  flex-wrap: wrap;
  max-width: var(--page);
  margin: clamp(16px, 2.5vw, 28px) auto 0;
  padding-bottom: 2px;
  border-bottom: 1px solid rgba(255, 255, 255, 0.10);
}

.tabs__tab {
  appearance: none;
  background: transparent;
  border: 0;
  border-bottom: 2px solid transparent;
  color: var(--text-dim);
  font: inherit;
  font-size: 14px;
  padding: 10px 16px;
  cursor: pointer;
  white-space: nowrap;
  transition: color 120ms ease, border-color 120ms ease, background 120ms ease;
}
.tabs__tab i { opacity: 0.75; margin-right: 4px; }

/* A drawn mark standing in for a font icon in the nav. Sits on the text
   baseline and carries the same dimmed weight the fa glyphs beside it have, so
   one drawing among a row of glyphs does not read as louder than the rest. */
.navmark { vertical-align: -0.22em; margin-right: 2px; opacity: 0.8; }
.bar-item:hover .navmark, .mobile-menu__item:hover .navmark { opacity: 1; }
.tabs__tab:hover { color: var(--text); background: rgba(255, 255, 255, 0.04); }
.tabs__tab.is-active {
  color: var(--text);
  border-bottom-color: var(--neon-green);
}
.tabs__tab.is-active i { opacity: 1; color: var(--neon-green); }

.tabs__panel { max-width: var(--page); margin: 0 auto; }

.profilesection { margin: clamp(20px, 3vw, 32px) 0; }
.profilesection > h3 { margin-bottom: 6px; }

.productgrid { display: flex; flex-wrap: wrap; gap: 16px; }

/* ------------------------------------------------------------- file lists */

.cloudgroup { margin-bottom: clamp(24px, 3.5vw, 40px); }
.cloudgroup > h3 { margin-bottom: 4px; }
.cloudgroup__count {
  font-size: 12px;
  color: var(--text-faint);
  border: 1px solid rgba(255, 255, 255, 0.14);
  border-radius: 999px;
  padding: 1px 8px;
  margin-left: 6px;
  vertical-align: middle;
}
.cloudgroup__note {
  font-size: 13px;
  color: var(--text-faint);
  margin: 0 0 12px;
  max-width: 76ch;
}

/* -- switching between the areas of your cloud --------------------------- */

/* A nested tab strip sits inside a panel that already has one, so it steps
   down: smaller type, a thinner rule, and no top border of its own. Without
   that the two rows read as equals and the reader cannot tell which one they
   are inside. */
.cloudareas .tabs { margin-top: 0; font-size: 13px; }
.cloudareas .tabs__tab { padding: 8px 12px; font-size: 13px; }
.cloudareas .tabs__panel { max-width: none; }

/* A drawn mark in place of a glyph, on the tab's own baseline.

   THE FAINT WHITE HALO IS FOR THE DARK MARKS. These come from five different
   places and are not a set: the Break and House ninja carries its own keyline,
   the 2.0 tile is a bright square, and the Hamst3r hamster is dark grey on
   transparency -- which on a dark tab strip is nearly the strip. One soft light
   edge lifts that one and is invisible under the others, which is cheaper than
   a rule per mark and does not need anybody to remember it for the sixth. */
.tabs__mark {
  vertical-align: -0.2em;
  margin-right: 4px;
  opacity: 0.85;
  object-fit: contain;
  filter: drop-shadow(0 0 1px rgba(255, 255, 255, 0.4));
}
.tabs__tab.is-active .tabs__mark { opacity: 1; }

/* The purple accent in a tab's name has to survive the ACTIVE state, where the
   label turns bright and the underline is neon: left as it is, "ezmuze
   *studio*" would keep a dim magenta word in an otherwise lit title. */
.tabs__tab.is-active .ezmuzePurple { color: var(--neon-magenta); text-shadow: none; }
/* Where you are inside an area. A real trail, unlike the 3.0 view's single "up"
   row: those folders are a field inside the file so there is only ever one
   level, and these are directories on disk that nest as deep as the game wrote
   them. */
.cloudcrumbs {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 4px;
  margin: 0 0 12px;
  font-size: 13px;
  color: var(--text-faint);
}
.cloudcrumbs > span { display: inline-flex; align-items: center; gap: 4px; }
.cloudcrumbs__crumb {
  appearance: none;
  background: transparent;
  border: 0;
  padding: 0;
  font: inherit;
  color: var(--text-dim);
  cursor: pointer;
}
.cloudcrumbs__crumb:hover { color: var(--text); text-decoration: underline; }
.cloudcrumbs__sep { font-size: 9px; opacity: 0.5; }
.cloudcrumbs__here { color: var(--text); }

.filelist { list-style: none; margin: 0; padding: 0; }

.filerow {
  border-bottom: 1px solid rgba(255, 255, 255, 0.06);
}
.filerow:hover { background: rgba(255, 255, 255, 0.03); }

.filerow__main {
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 8px 10px;
  min-height: 44px;
}

.filerow__play {
  flex: none;
  width: 30px; height: 30px;
  border-radius: 50%;
  border: 1px solid rgba(255, 255, 255, 0.16);
  background: rgba(255, 255, 255, 0.05);
  color: var(--text);
  cursor: pointer;
  font-size: 11px;
  display: grid; place-items: center;
}
.filerow__play:hover { background: var(--neon-green); color: #04140a; border-color: transparent; }

.filerow__name {
  appearance: none;
  background: transparent;
  border: 0;
  color: var(--text);
  font: inherit;
  font-size: 14px;
  text-align: left;
  cursor: pointer;
  flex: 1 1 auto;
  min-width: 0;
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 0;
}
.filerow__name > span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.filerow__name > i { font-size: 10px; color: var(--text-faint); flex: none; width: 10px; }
.filerow__name--static { cursor: default; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.filerow__icon { color: var(--neon-violet); font-size: 12px; margin-right: 2px; }

.filerow__rename { flex: 1 1 auto; min-width: 0; font-size: 14px; padding: 4px 8px; }

/* -- the cloud browser: folders and songs in one listing ------------------ */

/* Up, and the folders. Same row height as a song even though there is no
   40px cover in them, so the list does not jolt as you scroll past. */
.filerow--nav .filerow__folder {
  appearance: none;
  background: transparent;
  border: 0;
  color: var(--text);
  font: inherit;
  font-size: 14px;
  text-align: left;
  cursor: pointer;
  width: 100%;
  min-height: 58px;
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 0 12px;
}
.filerow--nav .filerow__folder:hover { background: rgba(255, 255, 255, 0.06); }
.filerow__foldericon {
  width: 40px;             /* the width a cover would take, so names line up */
  text-align: center;
  font-size: 18px;
  color: var(--blue);
  flex: none;
}
.filerow__foldercount { margin-left: auto; color: var(--text-faint); font-size: 0.85rem; }

/* A file in the game-area browser. It takes the SAME 40px column the folder
   icons above do -- and the 40px cover art does in the 3.0 view -- so names line
   up down the list whatever kind of row they are on. The element selector is
   there to win against `.filerow__name > i`, which sizes the 10px chevron. */
.filerow__name > i.filerow__fileicon {
  width: 40px;
  text-align: center;
  font-size: 14px;
  color: var(--text-faint);
  flex: none;
}
/* Something the game can actually open, as opposed to a sidecar sitting beside
   it. The one distinction the server can make honestly, so it is the one the
   colour carries. */
.filerow__name > i.filerow__fileicon.is-song { color: var(--neon-violet); }

/* The songs-only / everything switch, and the artist beside a song's title. */
.cloudfiles__toggle {
  display: flex;
  align-items: center;
  gap: 10px;
  flex-wrap: wrap;
  margin: 0 0 10px;
}
/* Secondary to the title and clearly not part of it -- these formats cannot
   state an author, so this is the sidecar talking, not the file. */
.cloudfiles__by {
  color: var(--text-faint);
  font-size: 0.8rem;
  flex: 0 1 auto;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.cloudfiles__by::before { content: "by "; opacity: 0.7; }

/* Files here have no cover art, so their rows would come out shorter than the
   folder rows they are listed with and the list would step as you scrolled. */
.filelist--files .filerow__main { min-height: 58px; }

/* The ... menu. Anchored to the row rather than floated: there is one open at
   a time and it belongs to a specific song, so it should move with it. */
.filerow__actions { position: relative; }
.filerow__more.is-on { background: var(--neutral); }
.filemenu {
  position: absolute;
  right: 0;
  top: calc(100% + 4px);
  z-index: 6;
  display: flex;
  flex-direction: column;
  align-items: stretch;
  gap: 4px;
  padding: 6px;
  min-width: 190px;
  /* --raised, the token for something sitting on top of a panel. A menu over
     a translucent panel has to be OPAQUE: the row underneath would otherwise
     read through the item you are trying to click. */
  background: var(--raised);
  border: 1px solid var(--border);
  box-shadow: 0 10px 30px -8px rgba(0, 0, 0, 0.8);
}
.filemenu .button { justify-content: flex-start; text-align: left; }

/* Said once, then gone. Sits at the top of the panel rather than floating over
   the corner of the window: the thing it is talking about is right here, and a
   message that travels away from its cause has to work harder to be understood. */
.cloudtoast {
  display: flex;
  align-items: center;
  gap: 8px;
  margin: 0 0 12px;
  padding: 10px 12px;
  background: rgba(58, 108, 74, 0.28);
  border-left: 3px solid var(--positive);
  color: var(--text);
  font-size: 0.9rem;
}

/* Whether a shared song has audio yet. */
.filerow__rendered  { color: var(--green); }
.filerow__rendering { color: var(--blue); }
.filerow__waiting   { color: var(--text-faint); }

.filerow__filingnote { color: var(--text-faint); font-size: 0.8rem; }

.filerow__facts {
  display: flex;
  gap: 10px;
  flex: none;
  font-size: 11px;
  color: var(--text-faint);
  font-variant-numeric: tabular-nums;
}

.filerow__actions { display: flex; gap: 6px; flex: none; flex-wrap: wrap; }

.btn--small { font-size: 12px; padding: 5px 10px; }
.btn--danger {
  border-color: rgba(255, 120, 120, 0.5);
  color: #ffb3b3;
  background: rgba(255, 70, 70, 0.10);
}
.btn--danger:hover { background: rgba(255, 70, 70, 0.22); color: #fff; }

.studiolink i { font-size: 10px; }

/* ----------------------------------------------------------- the expansion */

.filerow__detail {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(190px, 1fr));
  gap: 10px 24px;
  margin: 0;
  padding: 4px 10px 16px 32px;
}
.filerow__detail > div { min-width: 0; }
.filerow__detail dt {
  font-size: 10px;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--text-faint);
  margin-bottom: 2px;
}
.filerow__detail dd { margin: 0; font-size: 13px; color: var(--text-dim); }
.filerow__detail .filerow__uid { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 11px; }
.filerow__blurb { grid-column: 1 / -1; }
.filerow__blurb dd { white-space: pre-wrap; }

@media (max-width: 720px) {
  /* The actions wrap under the name rather than squeezing it to nothing. */
  .filerow__main { flex-wrap: wrap; row-gap: 8px; }
  .filerow__name, .filerow__name--static, .filerow__rename { flex: 1 1 100%; }
  .filerow__actions { flex: 1 1 100%; }
  .filerow__detail { padding-left: 10px; }
  .tabs { gap: 0; }
  .tabs__tab { padding: 10px 12px; font-size: 13px; }
}

.filerow__formats { grid-column: 1 / -1; }
.filerow__formats dd { display: flex; flex-wrap: wrap; gap: 5px; }
.chip {
  font-size: 11px;
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  color: var(--text-dim);
  border: 1px solid rgba(255, 255, 255, 0.14);
  border-radius: 999px;
  padding: 2px 9px;
  white-space: nowrap;
}

/* --------------------------------------------------- which studio to open */

.buildpick { border: 0; margin: 0 0 16px; padding: 0; }
.buildpick legend { font-size: 12px; opacity: 0.75; padding: 0; margin-bottom: 4px; }
.buildpick .editprofile__hint { margin: 0 0 8px; }

.buildpick__opt {
  display: flex;
  align-items: baseline;
  gap: 10px;
  padding: 8px 12px;
  border: 1px solid rgba(255, 255, 255, 0.10);
  border-radius: var(--radius);
  margin-bottom: 6px;
  cursor: pointer;
  transition: border-color 120ms ease, background 120ms ease;
}
.buildpick__opt:hover { background: rgba(255, 255, 255, 0.04); }
.buildpick__opt.is-on { border-color: var(--neon-green); background: rgba(39, 248, 54, 0.06); }
.buildpick__opt input { accent-color: var(--neon-green); }
.buildpick__opt span { font-size: 14px; }
.buildpick__opt small { font-size: 12px; color: var(--text-faint); margin-left: auto; }

/* The full product description, rendered from the `=` / `==` / `*` markup it
   is stored in -- see PitchText.tsx. Real block elements now, so no pre-wrap:
   the parser decides where the paragraphs are. */
.pitchlong {
  max-width: 68ch;
  color: var(--text-dim);
  margin-top: 14px;
}

.pitchlong p { margin: 0 0 14px; line-height: 1.6; }

/* The tagline the copy opens with. Sized up and un-dimmed, because it is the
   sentence the rest of the description is an argument for. */
.pitchlong__lead {
  font-size: clamp(15px, 1.5vw, 17px);
  color: var(--text);
  margin-bottom: 18px;
}

.pitchlong__head {
  font-size: 15px;
  letter-spacing: 0.06em;
  text-transform: lowercase;
  color: var(--text);
  margin: 26px 0 10px;
}
.pitchlong__head::before { content: '// '; color: var(--neon-green); }

.pitchlong__list { list-style: none; margin: 0 0 14px; padding: 0; }
.pitchlong__list li {
  position: relative;
  padding-left: 20px;
  line-height: 1.6;
  margin-bottom: 4px;
}
.pitchlong__list li::before {
  content: '';
  position: absolute;
  left: 4px;
  top: 0.62em;
  width: 6px;
  height: 6px;
  border-radius: 50%;
  background: var(--neon-green);
}

/* ========================================================== a product page */

.pdp {
  display: grid;
  grid-template-columns: auto minmax(0, 1fr);
  gap: clamp(24px, 4vw, 56px);
  align-items: start;
  max-width: var(--page);
  margin: clamp(20px, 3vw, 36px) auto 0;
}

.pdp__art { flex: none; }

.pdp__body { min-width: 0; }

.pdp__kicker {
  font-size: 11px;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--text-faint);
  margin-bottom: 6px;
}
.pdp__kicker i { color: var(--neon-green); margin-right: 5px; }

.pdp__body h1 { margin: 0 0 6px; line-height: 1.05; }

.pdp__by { font-size: 14px; color: var(--text-dim); margin-bottom: 16px; }
.pdp__by a { cursor: pointer; }

.pdp__pitch {
  font-size: clamp(15px, 1.5vw, 17px);
  line-height: 1.55;
  color: var(--text);
  max-width: 62ch;
  margin: 0 0 20px;
}

/* "read more…", which is a link's job done by a button.
   A button because it changes what is on this page rather than going
   anywhere; styled as a link because that is what it reads as, and because
   an actual button here would look like a second call to action beside the
   licence. The blurb it opens is rendered through PitchText, so a
   multi-paragraph description arrives as paragraphs rather than one wall. */
.pdp__more {
  background: none;
  border: 0;
  padding: 0;
  font: inherit;
  color: var(--blue);   /* the palette's link accent, same as a real link */
  cursor: pointer;
  white-space: nowrap;
}
.pdp__more:hover { text-decoration: underline; }

.pdp__pitch--open > *:first-child { margin-top: 0; }
.pdp__pitch--open > *:last-child { margin-bottom: 0; }

.pdp__retired { margin-bottom: 20px; }
.pdp__retired p { font-size: 14px; color: var(--text-faint); max-width: 62ch; }

.pdp__demo { display: block; width: min(100%, 420px); margin-bottom: 20px; }
.pdp__demo .previews { max-width: none; }

.pdp__nolicence { color: var(--text-faint); font-size: 14px; }

.pdp__long { max-width: var(--page); margin: clamp(22px, 3vw, 34px) auto 0; }

.pdp__related { max-width: var(--page); margin: clamp(26px, 3.5vw, 44px) auto 0; }
.pdp__related h3 { margin-bottom: 12px; }

.pdp__relatedgrid { display: flex; flex-wrap: wrap; gap: 18px; }

.reltile {
  appearance: none;
  background: transparent;
  border: 0;
  padding: 0;
  cursor: pointer;
  width: 116px;
  color: var(--text-dim);
  font: inherit;
  font-size: 12px;
  text-align: center;
  transition: transform 140ms ease, color 140ms ease;
}
.reltile:hover { transform: translateY(-3px); color: var(--text); }
.reltile span { display: block; margin-top: 8px; line-height: 1.3; }

/* ----------------------------------------------------------- the licences */

.pdp__licences { display: flex; flex-direction: column; gap: 10px; max-width: 620px; }

/* The purple table this replaces was a 43px bar with the name in one cell and
   the price floated to the far right of the other, which on a wide screen put
   the two halves of one decision a third of a metre apart. */
.licence {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  align-items: center;
  gap: 16px;
  padding: 14px 16px;
  border: 1px solid rgba(255, 255, 255, 0.10);
  border-radius: var(--radius);
  background: rgba(0, 0, 0, 0.28);
  transition: border-color 140ms ease, background 140ms ease;
}
.licence:hover { border-color: rgba(255, 255, 255, 0.20); background: rgba(0, 0, 0, 0.38); }

.licence.is-owned {
  border-color: rgba(39, 248, 54, 0.38);
  background: rgba(39, 248, 54, 0.06);
}

.licence__what { display: flex; flex-direction: column; gap: 3px; min-width: 0; }
.licence__name { font-size: 15px; color: var(--text); }
.licence__terms { font-size: 12px; color: var(--text-faint); }

.licence__act { flex: none; }
.licence__act .button { min-width: 116px; justify-content: center; }

.licence__owned {
  display: inline-flex;
  align-items: center;
  gap: 7px;
  font-size: 14px;
  color: var(--neon-green);
  padding: 0 10px;
}

.licence__said {
  grid-column: 1 / -1;
  margin: 2px 0 0;
  font-size: 13px;
  color: #ff9a9a;
}
.licence__said.is-good { color: var(--neon-green); }

@media (max-width: 780px) {
  /* The box stops being a column and becomes a header: at this width a 260px
     box beside anything leaves the text too narrow to read. */
  .pdp { grid-template-columns: 1fr; }
  .pdp__art { justify-self: center; }
  .licence { grid-template-columns: 1fr; gap: 10px; }
  .licence__act .button { width: 100%; }
}

/* ------------------------------------------------- a product's score: stars */

/* Read-only, filled to the fraction. Two rows of five stars laid over each
   other: the dim row is the scale, the lit row is clipped to the average, so
   4.3 shows a third of a fifth star rather than rounding to four or five.
   The rating a person GIVES is .stars (a vote) and .rpick (the review form);
   this is only ever a fact about the product. */
.rstars {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  font-variant-numeric: tabular-nums;
  vertical-align: middle;
}
.rstars__row { position: relative; display: inline-block; line-height: 1; white-space: nowrap; }
.rstars__off { color: rgba(255, 255, 255, 0.18); }
.rstars__on {
  position: absolute; top: 0; left: 0; overflow: hidden;
  color: var(--amber);
  white-space: nowrap;
}
.rstars__row i { font-size: 12px; margin-right: 1px; }
.rstars__avg { font-size: 13px; color: var(--text); }
.rstars__count { font-size: 12px; color: var(--text-faint); }

.rstars--small { gap: 4px; }
.rstars--small .rstars__row i { font-size: 10px; }
.rstars--small .rstars__avg { font-size: 11px; color: var(--text-dim); }
.rstars--small .rstars__count { font-size: 11px; }

.rstars--large { gap: 8px; }
.rstars--large .rstars__row i { font-size: 17px; margin-right: 2px; }
.rstars--large .rstars__on { filter: drop-shadow(0 0 6px rgba(255, 196, 64, 0.45)); }
.rstars--large .rstars__avg { font-family: Comfortaa, system-ui, sans-serif; font-weight: 700; font-size: 20px; }
.rstars--large .rstars__count { font-size: 15px; color: var(--text-dim); }
.rstars.is-none .rstars__off { color: rgba(255, 255, 255, 0.12); }

/* The picker: five buttons that light to the cursor, then to the choice. */
.rpick { display: inline-flex; align-items: center; gap: 2px; margin: 4px 0 14px; }
.rpick__s {
  background: none; border: 0; padding: 2px 3px; cursor: pointer;
  font-size: 22px; line-height: 1;
  color: rgba(255, 255, 255, 0.18);
  transition: color var(--t) var(--ease), transform var(--t) var(--ease);
}
.rpick__s.is-on { color: var(--amber); }
.rpick:hover .rpick__s { color: var(--amber); }
.rpick__s:hover ~ .rpick__s { color: rgba(255, 255, 255, 0.18); }
.rpick__s:hover { transform: scale(1.18); }
.rpick__s:disabled { cursor: default; }
.rpick__say { margin-left: 10px; font-size: 13px; color: var(--text-faint); }

/* -------------------------------------------------- a product page: reviews */

.pdp__rating { margin: -2px 0 16px; }
.pdp__rating a { color: inherit; text-decoration: none; cursor: pointer; }
.pdp__rating a:hover .rstars__count { color: var(--text); }

.pdp__reviews { max-width: var(--page); margin: clamp(26px, 3.5vw, 44px) auto 0; scroll-margin-top: 80px; }

.reviews__head { display: flex; align-items: baseline; gap: 18px; flex-wrap: wrap; margin-bottom: 10px; }
.reviews__head h3 { margin: 0; }

.reviews__cols {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(280px, 360px);
  gap: 22px;
  align-items: start;
  margin-top: 18px;
}
.reviews__list { display: flex; flex-direction: column; gap: 10px; min-width: 0; }
.reviews__empty { color: var(--text-faint); font-size: 14px; margin: 6px 0; }
.reviews__said { color: #ff9a9a; font-size: 13px; margin: 6px 0 0; }
.reviews__more { align-self: flex-start; margin-top: 4px; }

/* The histogram: five short bars, five-star first. A row, not a column,
   because it is a summary to glance at on the way to the words. */
.rhist { display: flex; flex-wrap: wrap; gap: 4px 18px; font-size: 12px; color: var(--text-dim); }
.rhist__row { display: inline-flex; align-items: center; gap: 6px; }
.rhist__star { white-space: nowrap; font-variant-numeric: tabular-nums; }
.rhist__star i { font-size: 9px; color: var(--amber); }
.rhist__bar { display: inline-block; width: 72px; height: 8px; border-radius: 4px; background: rgba(255, 255, 255, 0.08); overflow: hidden; }
.rhist__fill { display: block; height: 100%; background: var(--amber); opacity: 0.85; border-radius: 4px; transition: width 240ms var(--ease); }
.rhist__n { min-width: 1.5ch; font-variant-numeric: tabular-nums; }

/* One review. The stars, the title and who said it on one line; the words
   under it; what you can do about it at the foot, quiet until hovered. */
.review {
  padding: 12px 14px;
  border: 1px solid rgba(255, 255, 255, 0.09);
  border-radius: var(--radius);
  background: rgba(0, 0, 0, 0.24);
}
.review.is-mine { border-color: rgba(39, 248, 54, 0.30); }
.review.is-removed { border-style: dashed; opacity: 0.75; }
.review__head { display: flex; align-items: baseline; gap: 8px; flex-wrap: wrap; }
.review__title { font-weight: 600; color: var(--text); }
.review__meta { display: inline-flex; align-items: baseline; gap: 8px; flex-wrap: wrap; font-size: 12px; color: var(--text-faint); }
.review__who { color: var(--text-dim); cursor: pointer; }
.review__who:hover { color: var(--text); }
.review__you, .review__v, .review__gone {
  font-size: 11px; padding: 1px 7px; border-radius: 999px;
  border: 1px solid rgba(255, 255, 255, 0.14);
}
.review__you { color: var(--neon-green); border-color: rgba(39, 248, 54, 0.4); }
.review__gone { color: var(--text-faint); border-style: dashed; }
.review__body { margin: 6px 0 0; font-size: 14px; line-height: 1.5; color: var(--text-dim); white-space: pre-wrap; max-width: 72ch; }
.review__removed { margin: 6px 0 0; font-size: 13px; color: #ffb3b3; }
.review__onlyyou { color: var(--text-faint); }
.review__reply {
  margin: 10px 0 0; padding: 8px 12px;
  border-left: 2px solid var(--neon-cyan);
  background: rgba(90, 217, 255, 0.05);
  font-size: 13px;
}
.review__reply p { margin: 3px 0 0; color: var(--text-dim); white-space: pre-wrap; }
.review__replyby { font-size: 11px; color: var(--neon-cyan); letter-spacing: 0.04em; text-transform: uppercase; }
.review__acts { display: flex; gap: 12px; align-items: center; margin-top: 8px; min-height: 18px; }
.review__act {
  background: none; border: 0; padding: 0; cursor: pointer;
  font: inherit; font-size: 12px; color: var(--text-faint);
  transition: color var(--t) var(--ease);
}
.review__act:hover { color: var(--text); }
.review__thanks { font-size: 12px; color: var(--neon-green); }

/* Reporting one: the reason, asked for in place. */
.rreport { margin-top: 10px; padding-top: 10px; border-top: 1px dashed rgba(255, 255, 255, 0.12); }
.rreport__label { display: block; font-size: 12px; color: var(--text-faint); margin-bottom: 4px; }
.rreport__why {
  width: 100%; box-sizing: border-box; min-height: 64px; resize: vertical;
  padding: 8px 10px; font: inherit; font-size: 13px;
  border: 1px solid rgba(255, 255, 255, 0.16); border-radius: 8px;
  background: rgba(0, 0, 0, 0.42); color: var(--text);
}
.rreport__acts { display: flex; gap: 8px; align-items: center; margin-top: 8px; flex-wrap: wrap; }

/* Writing one. A card on the right, so the list and the form are two things
   side by side rather than a form wedged into a list. */
.rform {
  padding: 16px 18px;
  border: 1px solid rgba(255, 255, 255, 0.12);
  border-radius: var(--radius);
  background: rgba(0, 0, 0, 0.34);
}
.rform--note { font-size: 14px; color: var(--text-dim); }
.rform--note i { color: var(--neon-cyan); margin-right: 4px; }
.rform__own { color: var(--neon-green); }
.rform__own i { color: inherit; }
.rform__acts { display: flex; gap: 8px; justify-content: flex-end; margin-top: 12px; flex-wrap: wrap; }
.rform--note .rform__acts { justify-content: flex-start; }
.rform__head { display: flex; align-items: baseline; justify-content: space-between; gap: 10px; flex-wrap: wrap; margin-bottom: 4px; }
.rform__head h4 { margin: 0; font-size: 16px; }
.rform__owns {
  font-size: 11px; padding: 2px 9px; border-radius: 999px;
  color: var(--neon-green); border: 1px solid rgba(39, 248, 54, 0.4); background: rgba(39, 248, 54, 0.08);
}
.rform__field { display: block; margin-bottom: 12px; }
.rform__field > span { display: flex; justify-content: space-between; font-size: 12px; color: var(--text-faint); margin-bottom: 4px; }
.rform__field em { font-style: normal; font-variant-numeric: tabular-nums; }
.rform__field input, .rform__field textarea {
  width: 100%; box-sizing: border-box;
  padding: 9px 11px; font: inherit; font-size: 14px;
  border: 1px solid rgba(255, 255, 255, 0.16); border-radius: 8px;
  background: rgba(0, 0, 0, 0.42); color: var(--text);
}
.rform__field textarea { resize: vertical; min-height: 110px; }
.rform__field input:focus, .rform__field textarea:focus {
  outline: none; border-color: rgba(90, 217, 255, 0.55); box-shadow: 0 0 0 3px rgba(90, 217, 255, 0.12);
}

@media (max-width: 860px) {
  .reviews__cols { grid-template-columns: 1fr; }
  /* The form above the list on a phone: the thing you came to do, then the
     reading. */
  .reviews__side { order: -1; }
}

/* The store tile's stars sit on the by-line, after the name. */
.pcard__by { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }

/* ------------------------------------------------- admin: reported reviews */

.admreview {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(240px, 300px);
  gap: 16px;
  align-items: start;
  padding: 14px 0;
  border-top: 1px solid rgba(255, 255, 255, 0.08);
}
.admreview.is-removed .admreview__what { opacity: 0.7; }
.admreview.is-done { background: rgba(255, 255, 255, 0.02); }
.admreview__product { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; font-size: 13px; margin-bottom: 6px; }
.admreview__code { font-size: 11px; color: var(--text-faint); letter-spacing: 0.08em; }
.admreview__head { display: flex; align-items: baseline; gap: 8px; flex-wrap: wrap; }
.admreview__by { font-size: 12px; color: var(--text-faint); }
.admreview__body { margin: 6px 0; font-size: 14px; color: var(--text-dim); white-space: pre-wrap; }
.admreview__reports { list-style: none; margin: 8px 0 0; padding: 0; font-size: 13px; color: var(--text-dim); }
.admreview__reports li { margin: 2px 0; }
.admreview__reports i { color: #ffb3b3; font-size: 11px; }
/* The per-report dismiss button (#171's module queue) sits INSIDE the sentence
   it dismisses, so it has to read as a footnote rather than as a second action
   competing with "remove" on the right. */
.admreview__reports .button { padding: 1px 8px; font-size: 11px; margin-left: 4px; vertical-align: baseline; }
.admreview__acts { display: flex; flex-direction: column; gap: 8px; }
.admreview__acts .button { justify-content: center; }
.admreview__said { font-size: 13px; color: #ff9a9a; }
@media (max-width: 720px) { .admreview { grid-template-columns: 1fr; } }

/* ============================================ sigma: balance, top up, ledger */

/* The balance where a price is being read, rather than only in the nav corner. */
.sigmabar {
  display: flex;
  align-items: center;
  gap: 10px;
  flex-wrap: wrap;
  padding: 9px 14px;
  border: 1px solid rgba(255, 255, 255, 0.10);
  border-radius: var(--radius);
  background: rgba(0, 0, 0, 0.28);
  font-size: 13px;
  color: var(--text-faint);
}
.sigmabar--compact { padding: 7px 12px; font-size: 12px; }

.sigmabar__amount {
  display: inline-flex;
  align-items: center;
  gap: 7px;
  font-size: 15px;
  color: var(--text);
  font-variant-numeric: tabular-nums;
}
.sigmabar__amount i { color: var(--neon-green); font-size: 12px; }
.sigmabar__text { flex: 1 1 auto; min-width: 0; }
.sigmabar .button { flex: none; }

/* Not alarming, just honest: you cannot afford the thing you are looking at. */
.sigmabar.is-short { border-color: rgba(255, 190, 90, 0.34); background: rgba(255, 170, 60, 0.07); }
.sigmabar.is-short .sigmabar__text { color: #ffcf94; }

/* The store toolbar puts it on its own line under the categories. */
.storebar .sigmabar { flex: 1 1 240px; max-width: 340px; }

/* ---------------------------------------------------------------- the page */

/* The page's one capped column.
   .container is uncapped on purpose -- full-bleed slab, fluid padding -- so the
   cap has to be applied by whatever wants its blocks to line up. .sigmahead used
   to do this for itself, which left the header centred in a --page column while
   the top-up, cash-out and statement below it ran the full width, their left
   edges landing outside the balance above them. Capping the whole page instead
   is what .tabs__panel does for Profile. */
.sigmapage { max-width: var(--page); margin-inline: auto; }

.sigmahead { margin: clamp(18px, 3vw, 30px) 0 0; }
.sigmahead__text p { max-width: 62ch; color: var(--text-dim); }

.sigmahead__amount {
  font-family: Comfortaa, 'Trebuchet MS', Verdana, sans-serif;
  font-weight: 700;
  font-size: clamp(44px, 8vw, 84px);
  line-height: 1;
  margin: 2px 0 10px;
  background: var(--grad-brand);
  -webkit-background-clip: text;
  background-clip: text;
  color: transparent;
  font-variant-numeric: tabular-nums;
}

.sigmahead__totals {
  display: flex;
  gap: 22px;
  flex-wrap: wrap;
  margin-top: 14px;
  font-size: 13px;
  color: var(--text-faint);
}
.sigmahead__totals strong { color: var(--text); font-variant-numeric: tabular-nums; }

/* ------------------------------------------------------------------ top up */

.topup { display: flex; flex-wrap: wrap; align-items: center; gap: 14px; }

.topup__amounts { display: flex; flex-wrap: wrap; gap: 8px; }

.topup__amount {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 2px;
  min-width: 82px;
  padding: 10px 14px;
  border: 1px solid rgba(255, 255, 255, 0.12);
  border-radius: var(--radius);
  background: rgba(0, 0, 0, 0.28);
  color: var(--text-dim);
  font: inherit;
  cursor: pointer;
  transition: border-color 130ms ease, background 130ms ease, transform 130ms ease;
}
.topup__amount:hover { transform: translateY(-2px); border-color: rgba(255, 255, 255, 0.24); }
.topup__amount.is-on {
  border-color: var(--neon-green);
  background: rgba(39, 248, 54, 0.08);
  color: var(--text);
}
.topup__sigma { font-size: 17px; font-weight: 700; }
.topup__usd { font-size: 11px; color: var(--text-faint); }

.topup__go { min-height: 44px; }

/* ------------------------------------------------------------------ ledger */

.ledger__dir {
  flex: none;
  width: 26px; height: 26px;
  border-radius: 50%;
  display: grid; place-items: center;
  font-size: 10px;
}
.ledger__dir.is-in  { background: rgba(39, 248, 54, 0.14); color: var(--neon-green); }
.ledger__dir.is-out { background: rgba(255, 255, 255, 0.07); color: var(--text-faint); }

/* align-items:flex-start is doing real work: .filerow__name sets center for
   its normal single-line use, and once this becomes a COLUMN that centres the
   text horizontally instead of vertically. */
.ledger__what { display: flex; flex-direction: column; align-items: flex-start; gap: 2px; }
.ledger__what > span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.ledger__who { font-size: 11px; color: var(--text-faint); }

.ledger__amount {
  flex: none;
  font-variant-numeric: tabular-nums;
  font-size: 14px;
  min-width: 84px;
  text-align: right;
}
.ledger__amount.is-in  { color: var(--neon-green); }
.ledger__amount.is-out { color: var(--text-dim); }

@media (max-width: 720px) {
  .ledger__amount { min-width: 0; }
  .topup { flex-direction: column; align-items: stretch; }
  .topup__go { width: 100%; }
}

/* Under the store toolbar, and the same width as it, so the two read as one
   block rather than a bar with a smaller bar tucked under its left corner.
   Full width without right-aligning the contents WOULD put the balance and the
   button at opposite ends of a 1400px screen -- the mistake the old licence row
   made -- so the group sits together at the right, under the sort control it
   belongs beside. */
/* In the toolbar row, sized by its own text. Full width with the text pushed
   right, which is what it was, drew a wide empty box with a sentence in the
   corner. */
.storebalance { flex: 0 0 auto; max-width: 100%; }
.storebalance .sigmabar { justify-content: flex-end; }
.storebalance .sigmabar__text { flex: 0 0 auto; }

/* On a product page there is no search and no sort, so the balance is the whole
   row and has to push itself over. Anywhere else the sort's own auto margin has
   already taken the slack, and a second one here would split it between them --
   leaving the sort menu stranded in the middle of the row. */
.storebalance:first-child { margin-left: auto; }

/* ------------------------------------------------------------ supporter badge */

.supbadge {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  vertical-align: middle;
  font-size: 11px;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: #ffb3e6;
  border: 1px solid rgba(255, 5, 199, 0.38);
  background: rgba(255, 5, 199, 0.10);
  border-radius: 999px;
  padding: 3px 10px;
  white-space: nowrap;
}
.supbadge i { font-size: 10px; color: var(--neon-magenta); }
.supbadge__word { line-height: 1; }

/* On an avatar it is a pip in the corner, opposite the claimed tick so the two
   never sit on top of each other. */
.supbadge--avatar {
  position: absolute;
  right: -2px; bottom: -2px;
  padding: 0;
  width: 22px; height: 22px;
  border-radius: 50%;
  justify-content: center;
  background: #17040f;
  border-color: rgba(255, 5, 199, 0.55);
  box-shadow: 0 0 10px rgba(255, 5, 199, 0.55);
}
.supbadge--avatar i { font-size: 9px; }

/* The avatar has to be a positioning context for the pip above -- it was static,
   so the badge would otherwise hang off whichever ancestor happened to be
   positioned, which is the page in most cases.
   
   And it must NOT clip: .avatar is overflow:hidden so a rectangular photo comes
   out circular, which also swallowed a pip sitting on the rim. The clipping
   moves onto the image and the initials, which are the only things that need
   it, leaving the badge free to overhang the edge the way it should. */
.avatar { position: relative; overflow: visible; }
.avatar img, .avatar__initials { border-radius: 50%; }
.avatar__initials { width: 100%; height: 100%; display: grid; place-items: center; }

/* ============================================================ the header bar */

/* THE BAR WAS FULL-BLEED WHILE EVERYTHING ELSE IS IN A var(--page) COLUMN, so
   the wordmark sat hard against the left edge of the screen and the links
   against the right, neither lining up with a single thing below them. The
   painted surface stays full width -- it should -- and its CONTENTS move into
   the same column as the page. */
/* NOT BLURRED EITHER, and for a stronger reason than the panels: this bar is
   position: relative and scrolls away with the page, so nothing ever passes
   underneath it. A backdrop filter on a surface that is never in front of
   anything moving cannot be seen under any condition -- verified by
   screenshotting it mid-scroll, where the bar is simply not on screen.
   (.playerbar keeps its blur. That one IS fixed, content really does slide
   under it, and a thin strip costs nothing.) */
.top {
  background: var(--header-a);
  border-bottom: 1px solid rgba(255, 255, 255, 0.08);
}
.top::after {
  content: "";
  position: absolute;
  left: 0; right: 0; bottom: -1px;
  height: 1px;
  background: var(--grad-brand);
  opacity: 0.5;
}

.top .bar {
  display: flex;
  align-items: center;
  gap: 2px;
  max-width: var(--page);
  margin: 0 auto;
  padding: 0 4px;
  background: transparent !important;
  border-bottom: 0;
  box-shadow: none;
  -webkit-backdrop-filter: none;
  backdrop-filter: none;
}
.top .bar::after { display: none; }

/* The legacy layout floated these; flex wants them not to. */
.top .bar .bar-item-black,
.top .bar .bar-item.button { float: none; }
.top .bar .right {
  float: none;
  margin-left: auto;
  display: flex;
  align-items: center;
  gap: 2px;
}

/* -- the wordmark --------------------------------------------------------- */

.bar .brand {
  display: inline-flex;
  align-items: baseline;
  gap: 8px;
  font-family: Comfortaa, system-ui, sans-serif;
  font-weight: 700;
  font-size: 19px;
  /* The old 0.34em tracking was set for the single word "ezmuze"; two words at
     that spacing read as two unrelated things rather than one name. */
  letter-spacing: 0.10em;
  padding: 8px 10px;
}
.brand__ezmuze { color: var(--text); }
.brand__central {
  color: var(--neon-violet);
  text-shadow: 0 0 14px rgba(160, 107, 255, 0.45);
}

@media (max-width: 520px) {
  .bar .brand { font-size: 16px; letter-spacing: 0.06em; gap: 6px; }
}

/* ------------------------------------------------------------ sort dropdown */

.sortmenu { position: relative; }
.sortmenu__button { display: inline-flex; align-items: center; gap: 8px; }
.sortmenu__caret { font-size: 10px; opacity: 0.7; }

.sortmenu__panel {
  position: absolute;
  right: 0;
  top: calc(100% + 6px);
  z-index: 30;
  min-width: 190px;
  padding: 6px;
  border: 1px solid rgba(255, 255, 255, 0.12);
  border-radius: var(--radius);
  background: #14151a;
  box-shadow: 0 18px 40px rgba(0, 0, 0, 0.6);
}

.sortmenu__item {
  display: flex;
  align-items: center;
  gap: 9px;
  width: 100%;
  padding: 9px 10px;
  border: 0;
  border-radius: 4px;
  background: transparent;
  color: var(--text-dim);
  font: inherit;
  font-size: 14px;
  text-align: left;
  cursor: pointer;
}
.sortmenu__item:hover { background: rgba(255, 255, 255, 0.06); color: var(--text); }
.sortmenu__item.is-on { color: var(--text); }
.sortmenu__item i { font-size: 11px; width: 12px; color: var(--neon-green); }
/* Reserves the tick's width so the labels do not shuffle sideways when the
   selection moves. */
.sortmenu__item i.is-hidden { visibility: hidden; }

.sortmenu__rule { height: 1px; margin: 5px 4px; background: rgba(255, 255, 255, 0.10); }

/* ================================================================= footer */

.sitefoot {
  position: relative;
  z-index: 1;
  max-width: var(--page);
  margin: clamp(40px, 6vw, 80px) auto 0;
  padding: clamp(24px, 3vw, 36px) 16px clamp(20px, 3vw, 28px);
  border-top: 1px solid rgba(255, 255, 255, 0.08);
  color: var(--text-faint);
  font-size: 13px;
}

.sitefoot__inner {
  display: grid;
  grid-template-columns: minmax(0, 1.5fr) minmax(0, 2fr);
  gap: clamp(20px, 4vw, 56px);
  align-items: start;
}

.sitefoot__brand .brand__ezmuze,
.sitefoot__brand .brand__central {
  font-family: Comfortaa, system-ui, sans-serif;
  font-weight: 700;
  font-size: 17px;
  letter-spacing: 0.08em;
}
.sitefoot__brand p { margin: 10px 0 0; max-width: 46ch; line-height: 1.6; }

.sitefoot__links {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
  gap: 20px;
}
.sitefoot__links h4 {
  margin: 0 0 8px;
  font-size: 11px;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--text-dim);
}
.sitefoot__links a {
  display: block;
  padding: 3px 0;
  color: var(--text-faint);
  transition: color 120ms ease;
}
.sitefoot__links a:hover { color: var(--text); }

.sitefoot__copy { margin: 14px 0 0; font-size: 12px; color: var(--text-faint); }

@media (max-width: 720px) {
  .sitefoot__inner { grid-template-columns: 1fr; }
}

/* ------------------------------------------------------------- legal pages */

.legal { max-width: 74ch; margin: clamp(20px, 3vw, 34px) auto 0; }
.legal h1 { margin-bottom: 4px; }
.legal h3 { margin: clamp(22px, 3vw, 32px) 0 8px; }
.legal p, .legal li { line-height: 1.7; color: var(--text-dim); }
.legal ul { padding-left: 20px; }
.legal li { margin-bottom: 8px; }
.legal strong { color: var(--text); }

.legal__updated { font-size: 12px; color: var(--text-faint); margin-top: 0; }
.legal__lede { font-size: 16px; color: var(--text); }
.legal__seealso { margin-top: 26px; }
.legal__foot {
  margin-top: clamp(26px, 4vw, 40px);
  padding-top: 16px;
  border-top: 1px solid rgba(255, 255, 255, 0.08);
  font-size: 13px;
  color: var(--text-faint);
}

/* ====================================================== the vault front page */

.vhero {
  display: grid;
  grid-template-columns: auto minmax(0, 1fr) auto;
  gap: clamp(20px, 4vw, 44px);
  align-items: center;
  max-width: var(--page);
  margin: clamp(18px, 3vw, 32px) auto clamp(26px, 4vw, 44px);
  padding: clamp(20px, 3vw, 34px);
  border-radius: var(--radius);
  border: 1px solid rgba(255, 255, 255, 0.08);
  /* The brand gradient at low opacity, so the hero reads as the loudest thing
     on the page without becoming a colour field. */
  background:
    radial-gradient(120% 140% at 0% 0%, rgba(160, 107, 255, 0.20), transparent 60%),
    radial-gradient(120% 140% at 100% 100%, rgba(39, 248, 54, 0.12), transparent 60%),
    rgba(0, 0, 0, 0.34);
}

.vhero__text { min-width: 0; }
.vhero__text h1 { margin: 2px 0 6px; line-height: 1.03; }
.vhero__facts { font-size: 13px; color: var(--text-dim); margin: 0 0 16px; }
.vhero__facts i { color: var(--neon-green); }

.vhero__tracks {
  list-style: none;
  margin: 18px 0 0;
  padding: 0;
  display: grid;
  gap: 2px;
  max-width: 520px;
}
.vhero__tracks button {
  display: flex;
  align-items: center;
  gap: 10px;
  width: 100%;
  padding: 6px 8px;
  border: 0;
  border-radius: 4px;
  background: transparent;
  color: var(--text-dim);
  font: inherit;
  font-size: 13px;
  text-align: left;
  cursor: pointer;
}
.vhero__tracks button:hover { background: rgba(255, 255, 255, 0.06); color: var(--text); }
.vhero__tracks i { font-size: 9px; color: var(--neon-green); }

.vhero--playlist { align-items: center; }

/* ---------------------------------------------------------------- sections */

.vsection { max-width: var(--page); margin: 0 auto clamp(28px, 4vw, 46px); }

.vsection__head {
  display: flex;
  align-items: flex-end;
  justify-content: space-between;
  gap: 16px;
  margin-bottom: 12px;
}
.vsection__head h2 { margin: 0; }
.vsection__note { margin: 4px 0 0; font-size: 13px; color: var(--text-faint); max-width: 68ch; }

/* ------------------------------------------------------------------ charts */

.chart { list-style: none; margin: 0; padding: 0; }

.chartrow {
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 7px 10px;
  border-radius: var(--radius);
}
.chartrow:hover { background: rgba(255, 255, 255, 0.04); }
.chartrow.is-playing { background: rgba(39, 248, 54, 0.07); }

.chartrow__rank {
  flex: none;
  width: 22px;
  text-align: right;
  font-size: 13px;
  color: var(--text-faint);
  font-variant-numeric: tabular-nums;
}

.chartrow__play {
  flex: none;
  width: 32px; height: 32px;
  border-radius: 50%;
  border: 1px solid rgba(255, 255, 255, 0.14);
  background: rgba(255, 255, 255, 0.05);
  color: var(--text);
  font-size: 11px;
  display: grid; place-items: center;
  cursor: pointer;
  transition: background 120ms ease, color 120ms ease;
}
.chartrow__play:hover { background: var(--neon-green); color: #04140a; border-color: transparent; }

.chartrow__what { display: flex; flex-direction: column; gap: 1px; flex: 1 1 auto; min-width: 0; }
.chartrow__name { font-size: 14px; color: var(--text); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.chartrow__by { font-size: 12px; color: var(--text-faint); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.chartrow__by:hover { color: var(--text); }

.chartrow__facts {
  flex: none;
  display: flex;
  gap: 12px;
  font-size: 11px;
  color: var(--text-faint);
  font-variant-numeric: tabular-nums;
}
.chartrow__facts i { font-size: 9px; margin-right: 3px; }

.chartrow__acts { flex: none; display: flex; gap: 4px; }

/* -------------------------------------------------------- the track buttons */

.trackbtn {
  width: 30px; height: 30px;
  border-radius: 50%;
  border: 1px solid transparent;
  background: transparent;
  color: var(--text-faint);
  font-size: 12px;
  display: grid; place-items: center;
  cursor: pointer;
  transition: background 120ms ease, color 120ms ease;
}
.trackbtn:hover { background: rgba(255, 255, 255, 0.09); color: var(--text); }
.trackbtn--small { width: 26px; height: 26px; font-size: 11px; }
.trackbtn__said { font-size: 11px; color: var(--neon-green); margin-left: 4px; }

.sharebox { position: relative; display: inline-flex; align-items: center; }

/* A menu rendered into <body> and placed from its button's rect, so nothing
   between the two can clip it. Fixed, not absolute: it is no longer inside
   the thing it belongs to, and the coordinates it is given are the
   viewport's. See components/Popover.tsx. */
.popover {
  position: fixed;
  z-index: 400;
}

/* Reuses the sort menu's items, which is the same thing in a different place. */
.sharemenu {
  min-width: 210px;
  padding: 6px;
  border: 1px solid rgba(255, 255, 255, 0.12);
  border-radius: var(--radius);
  background: #14151a;
  box-shadow: 0 18px 40px rgba(0, 0, 0, 0.6);
}
.sharemenu a.sortmenu__item { text-decoration: none; }
.sharemenu__note { padding: 9px 10px; font-size: 13px; color: var(--text-faint); }
.sharemenu__count { margin-left: auto; font-size: 11px; color: var(--text-faint); }
.sharemenu__new { display: flex; gap: 6px; padding: 6px; }
.sharemenu__new .input { flex: 1 1 auto; min-width: 0; font-size: 13px; padding: 6px 8px; }

/* The row is a click target for playing; the buttons on it are not. */
.track__acts { display: inline-flex; gap: 2px; margin-right: 6px; }

/* --------------------------------------------------------------- playlists */

.plgrid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
  gap: 16px;
}

/* No padded box round the art any more: the cover reaches the tile's edges,
   the way a cover does. The frame and the padding were doing the work of
   making a violet icon look deliberate, and there is a picture there now. */
.pltile {
  display: flex;
  flex-direction: column;
  color: var(--text-dim);
  transition: transform 140ms ease;
}
.pltile:hover { transform: translateY(-3px); }
.pltile__link { display: flex; flex-direction: column; gap: 4px; color: inherit; }
.pltile__link:hover { color: inherit; }
/* Outside the link, so rating one does not navigate to it. */
.pltile__rate { display: flex; margin-top: 4px; min-height: 18px; }

/* THE MOSAIC. Nine covers, edge to edge, no gap and no border between them --
   the tiles are the picture, and a line through it would read as nine small
   covers rather than one made of nine. line-height: 0 because the cells are
   inline-level and would otherwise sit on a text baseline with a gap under
   each row that no amount of gap: 0 would remove. */
.plart {
  display: grid;
  grid-template-columns: repeat(var(--plart-cols, 3), 1fr);
  gap: 0;
  width: 100%;
  aspect-ratio: 1 / 1;
  line-height: 0;
  overflow: hidden;
  border-radius: var(--radius);
  box-shadow: 0 10px 26px -12px rgba(0, 0, 0, 0.9);
}
.plart .cover { border-radius: 0; }
.plart--big { width: 190px; }
/* The album tiles in the artist's own manager hold a case, so the grid that
   lays out a playlist mosaic does not apply and the box must not be clipped. */
.plart--case {
  display: grid;
  place-items: center;
  aspect-ratio: auto;
  overflow: visible;
}

/* An empty playlist really has nothing to show, so it keeps the old icon. */
.plart--none {
  display: grid;
  grid-template-columns: 1fr;
  place-items: center;
  font-size: 26px;
  color: #0b0c10;
  background: linear-gradient(135deg, var(--neon-violet), var(--neon-magenta));
}

.pltile__name { font-size: 14px; color: var(--text); }
.pltile__count { font-size: 11px; color: var(--text-faint); }

/* A name, not a link that announces itself. The underline and the blue would
   fight a 64px heading; it is plainly clickable from being a name at that
   size, and the hover says the rest. */
/* The artist's name inside one of their records: the way back to everything
   they made. A button because it changes a query parameter rather than going
   anywhere new, and styled as the heading it is -- the underline and the blue
   would fight type this size, and it is plainly clickable from being a name at
   the top of a page you have gone one level into. */
.artisthead__up {
  appearance: none; border: 0; background: transparent; padding: 0;
  font: inherit; color: inherit; cursor: pointer; text-align: left;
}
.artisthead__up:hover { color: var(--brand-soft); }

.vhero__who { color: inherit; text-decoration: none; }
.vhero__who:hover { color: var(--brand-soft); }

.vhero__rate { display: flex; align-items: center; gap: 10px; margin: 2px 0 12px;
               font-size: 13px; color: var(--text-faint); }

.plnew { display: flex; gap: 10px; margin: 16px 0 24px; max-width: 480px; }
.plnew .input { flex: 1 1 auto; min-width: 0; }
.plname { font-size: 28px; padding: 4px 10px; max-width: 460px; }

@media (max-width: 760px) {
  .vhero { grid-template-columns: 1fr; justify-items: start; }
  .chartrow__facts { display: none; }
  .vsection__head { flex-direction: column; align-items: flex-start; }
}

/* ================================================================== shelves */

.shelf { max-width: var(--page); margin: 0 auto clamp(26px, 3.5vw, 42px); }

/* Above the rail, which now reaches up under it by 20px: without this the
   rail's padding would sit over the arrows and swallow the clicks. */
.shelf__head {
  position: relative;
  z-index: 1;
  display: flex;
  align-items: flex-end;
  justify-content: space-between;
  gap: 16px;
  margin-bottom: 12px;
}
.shelf__title h2 { margin: 0; }
.shelf__note { margin: 4px 0 0; font-size: 13px; color: var(--text-faint); max-width: 70ch; }

.shelf__controls { display: flex; align-items: center; gap: 6px; flex: none; }

.shelf__arrow {
  width: 32px; height: 32px;
  border-radius: 50%;
  border: 1px solid rgba(255, 255, 255, 0.14);
  background: rgba(0, 0, 0, 0.4);
  color: var(--text-dim);
  font-size: 11px;
  display: grid; place-items: center;
  cursor: pointer;
  transition: background 120ms ease, color 120ms ease, opacity 120ms ease;
}
.shelf__arrow:hover:not(:disabled) { background: rgba(255, 255, 255, 0.12); color: var(--text); }
/* Hidden rather than greyed at the ends: an arrow pointing at nothing is
   furniture, and on a touch screen it is never used anyway. */
.shelf__arrow:disabled { opacity: 0; pointer-events: none; }

/* ROOM FOR A CASE TO GROW INTO. A rail scrolls sideways, and a box that scrolls
   on one axis clips on the other -- overflow-y computes to auto the moment
   overflow-x is not visible, so there is no declaration that lets a hovered
   tile out. The album cases zoom about a fifth on hover, roughly 17px each way
   at shelf size, and were being sliced off top and bottom.
   The padding gives them that room inside the clip and the negative margin
   takes it back out of the layout, so nothing moved: 24 - 20 is the 4px of top
   padding the rail always had, and 30 - 20 the 10px at the bottom. */
.shelf__rail {
  display: flex;
  gap: 16px;
  overflow-x: auto;
  scroll-snap-type: x proximity;
  padding: 42px 20px 48px;
  margin: -38px -20px -38px;
  /* AND THE SNAP HAS TO BE TOLD, or the horizontal half of that does nothing.
     The items are scroll-snap-align: start, so the browser pulls the first one
     to the edge of the snapport -- which is the padding box -- and scrolls the
     rail by exactly the padding, cancelling it: measured scrollLeft: 20 with
     20px of padding, and the first case still clipped by the same 8.7px it was
     before. scroll-padding insets the snapport to match, so the first item
     snaps AFTER the padding and the room stays where it was put. */
  scroll-padding-inline: 20px;
  /* The scrollbar is noise under a row of covers; the arrows and the drag do
     the job. Still scrollable by every input. */
  scrollbar-width: none;
}
.shelf__rail::-webkit-scrollbar { display: none; }
.shelf__rail > * { scroll-snap-align: start; }

/* --------------------------------------------------------------- a track */

.tcard {
  flex: 0 0 auto;
  width: 168px;
  display: flex;
  flex-direction: column;
  color: var(--text-dim);
}

.tcard__art {
  position: relative;
  width: 168px;
  height: 168px;
  border-radius: var(--radius);
  overflow: hidden;
  box-shadow: 0 10px 26px -12px rgba(0, 0, 0, 0.9);
}
.tcard__art .cover { display: block; width: 100%; height: 100%; }

/* The play button rides in on hover, the way a cover grid always has. It is
   always present for keyboard and touch -- only its opacity moves. */
.tcard__play {
  position: absolute;
  right: 8px; bottom: 8px;
  width: 42px; height: 42px;
  border-radius: 50%;
  border: 0;
  background: var(--neon-green);
  color: #04140a;
  font-size: 14px;
  display: grid; place-items: center;
  cursor: pointer;
  opacity: 0;
  transform: translateY(6px);
  transition: opacity 140ms ease, transform 140ms ease, background 140ms ease;
  box-shadow: 0 8px 20px -6px rgba(0, 0, 0, 0.8);
}
.tcard:hover .tcard__play,
.tcard:focus-within .tcard__play,
.tcard.is-playing .tcard__play { opacity: 1; transform: none; }
.tcard__play:hover { background: #4bff58; }

.tcard__acts {
  position: absolute;
  left: 6px; top: 6px;
  display: flex;
  gap: 2px;
  opacity: 0;
  transition: opacity 140ms ease;
}
.tcard:hover .tcard__acts, .tcard:focus-within .tcard__acts { opacity: 1; }
.tcard__acts .trackbtn {
  background: rgba(0, 0, 0, 0.55);
  color: #fff;
  backdrop-filter: blur(6px);
}
.tcard__acts .trackbtn:hover { background: rgba(0, 0, 0, 0.8); }

.tcard.is-playing .tcard__art { outline: 2px solid var(--neon-green); outline-offset: -2px; }

.tcard__name {
  margin: 10px 0 2px;
  font-size: 14px;
  font-weight: 400;
  color: var(--text);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.tcard__by {
  font-size: 12px;
  color: var(--text-faint);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.tcard__by:hover { color: var(--text); }
.tcard__facts { margin-top: 4px; display: flex; gap: 10px; font-size: 11px; color: var(--text-faint); }
.tcard__facts i { font-size: 9px; margin-right: 3px; }

/* While the fetch is out, so the page has its shape before it has its data. */
.tcard--ghost {
  height: 168px;
  border-radius: var(--radius);
  background: linear-gradient(100deg, rgba(255,255,255,0.04), rgba(255,255,255,0.08), rgba(255,255,255,0.04));
  background-size: 200% 100%;
  animation: shimmer 1.4s linear infinite;
}
@keyframes shimmer { from { background-position: 200% 0 } to { background-position: -200% 0 } }

/* -------------------------------------------------------------- an artist */

.acard {
  flex: 0 0 auto;
  width: 148px;
  display: flex;
  flex-direction: column;
  align-items: center;
  text-align: center;
  color: var(--text-dim);
  transition: transform 140ms ease;
}
.acard:hover { transform: translateY(-4px); }
.acard .avatar { box-shadow: 0 10px 26px -12px rgba(0, 0, 0, 0.9); }
.acard__name {
  margin: 12px 0 2px;
  font-size: 14px;
  font-weight: 400;
  color: var(--text);
  max-width: 100%;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.acard__count { font-size: 12px; color: var(--text-faint); }

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

.vhero__covers { display: flex; gap: 8px; align-self: center; }
.vhero__covers button {
  border: 0; padding: 0; background: none; cursor: pointer; line-height: 0;
  border-radius: 6px; overflow: hidden;
  box-shadow: 0 8px 20px -10px rgba(0, 0, 0, 0.9);
  transition: transform 140ms ease;
}
.vhero__covers button:hover { transform: translateY(-3px); }

.vaultfoot {
  max-width: var(--page);
  margin: 0 auto;
  font-size: 13px;
  color: var(--text-faint);
}

@media (max-width: 900px) {
  .vhero__covers { display: none; }
}
@media (max-width: 620px) {
  .tcard, .tcard__art { width: 140px; }
  .tcard__art { height: 140px; }
  .acard { width: 124px; }
  /* Arrows are a pointer affordance; a thumb just swipes. */
  .shelf__arrow { display: none; }
}

@media (max-width: 620px) {
  /* Avatar sizes are inline (the component takes a px size), so overriding them
     for a narrow screen needs !important. A 168px hero portrait on a 390px
     phone was half the viewport before a single track was visible. */
  .vhero .avatar { width: 104px !important; height: 104px !important; font-size: 34px !important; }
  .vhero { padding: 18px; gap: 16px; }
  .vhero__text h1 { font-size: 30px; }
}

/* ================================================================== covers */

.cover {
  position: relative;
  flex: none;
  border-radius: inherit;
  overflow: hidden;
  line-height: 0;
}
/* Sized by its container rather than by the size prop -- the playlist
   mosaic's cells, which are a third of a box only CSS knows the width of. */
.cover--fill { width: 100%; height: 100%; }

.cover__img, .cover__svg {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  display: block;
}
/* An uploaded cover may not be exactly square if it came from somewhere other
   than our own cropper; cover rather than stretch it. */
.cover__img { object-fit: cover; }

/* -------------------------------------------- a cover as a play button */

.filerow__cover, .track__cover {
  position: relative;
  flex: none;
  width: 44px; height: 44px;
  padding: 0;
  border: 0;
  border-radius: 5px;
  overflow: hidden;
  background: transparent;
  cursor: pointer;
}
.track__cover { width: 40px; height: 40px; }

.filerow__coverplay, .track__coverplay {
  position: absolute;
  inset: 0;
  display: grid;
  place-items: center;
  background: rgba(0, 0, 0, 0.55);
  color: #fff;
  font-size: 12px;
  opacity: 0;
  transition: opacity 120ms ease;
}
.filerow:hover .filerow__coverplay,
.track:hover .track__coverplay,
.track.is-playing .track__coverplay,
.filerow__cover:focus-visible .filerow__coverplay { opacity: 1; }

/* ------------------------------------------------------- choosing a cover */

.coverpick {
  display: flex;
  gap: 16px;
  align-items: flex-start;
  padding: 14px 10px 18px 32px;
  border-top: 1px dashed rgba(255, 255, 255, 0.08);
}
.coverpick .cover { border-radius: var(--radius); }
.coverpick__acts { display: flex; flex-wrap: wrap; gap: 8px; align-items: center; }
.coverpick__hint, .coverpick__error {
  flex: 1 1 100%;
  margin: 2px 0 0;
  font-size: 12px;
  color: var(--text-faint);
}
.coverpick__error { color: #ff9a9a; }

.coverpick__check {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  font-size: 13px;
  color: var(--text-dim);
  cursor: pointer;
  padding: 6px 10px;
  border: 1px solid rgba(255, 255, 255, 0.10);
  border-radius: var(--radius);
}
.coverpick__check:hover { border-color: rgba(255, 255, 255, 0.2); color: var(--text); }
.coverpick__check input { accent-color: var(--neon-green); }

@media (max-width: 620px) {
  .coverpick { padding-left: 10px; }
}

/* ============================================================= a track page */

.trackpage {
  display: grid;
  grid-template-columns: auto minmax(0, 1fr);
  gap: clamp(22px, 4vw, 48px);
  align-items: end;
  max-width: var(--page);
  margin: clamp(14px, 2vw, 24px) auto clamp(24px, 3vw, 36px);
}
.trackpage__art .cover { border-radius: var(--radius); box-shadow: 0 22px 50px -20px rgba(0,0,0,0.95); }
.trackpage__body { min-width: 0; }
.trackpage__body h1 { margin: 2px 0 12px; line-height: 1.02; }

.trackpage__by {
  display: inline-flex;
  align-items: center;
  gap: 10px;
  color: var(--text);
  font-size: 15px;
  margin-bottom: 10px;
}
.trackpage__by:hover { color: #fff; }

.trackpage__facts { font-size: 13px; color: var(--text-faint); margin: 0 0 18px; }
.trackpage__actions { margin-bottom: 18px; }
.trackpage__actions .trackbtn { width: 38px; height: 38px; border: 1px solid rgba(255,255,255,0.14); }

.trackpage__rate {
  display: flex;
  align-items: center;
  gap: 12px;
  font-size: 13px;
  color: var(--text-faint);
  flex-wrap: wrap;
}

.trackpage__artist {
  display: flex;
  gap: 18px;
  align-items: flex-start;
  max-width: var(--page);
  margin: 0 auto clamp(26px, 4vw, 40px);
  padding: clamp(16px, 2.2vw, 22px);
  border: 1px solid rgba(255, 255, 255, 0.08);
  border-radius: var(--radius);
  background: rgba(0, 0, 0, 0.28);
}
.trackpage__artist h3 { margin: 0 0 4px; }
.trackpage__artistfacts { margin: 0; font-size: 13px; color: var(--text-faint); }
.trackpage__artistfacts i { color: var(--neon-green); }
.trackpage__bio { margin: 10px 0 0; color: var(--text-dim); max-width: 68ch; white-space: pre-wrap; }

/* -------------------------------------- a cover that is also a play button */

.trackcover {
  position: relative;
  flex: none;
  padding: 0;
  border: 0;
  border-radius: 5px;
  overflow: hidden;
  background: transparent;
  cursor: pointer;
  line-height: 0;
}
.trackcover__play {
  position: absolute;
  inset: 0;
  display: grid;
  place-items: center;
  font-size: 12px;
  color: #fff;
  /* ALWAYS THERE, not on hover. A touch screen has no hover, and the whole
     point of a cover this small is that it is the play button -- a control that
     only exists once you have already guessed it is there is not a control. */
  background: rgba(0, 0, 0, 0.42);
  opacity: 1;
  transition: background 120ms ease;
}
.trackcover:hover .trackcover__play { background: rgba(0, 0, 0, 0.66); }

/* The big cards: the play button is always visible too, and only grows on
   hover, rather than appearing from nothing. */
.tcard__play { opacity: 0.86; transform: none; }
.tcard:hover .tcard__play, .tcard:focus-within .tcard__play { opacity: 1; transform: scale(1.06); }
.tcard__art { cursor: pointer; }
.tcard__name { display: block; color: var(--text); }
.tcard__name:hover { color: #fff; text-decoration: underline; }

/* ------------------------------------------------------------- the player */

.playerbar__cover { flex: none; line-height: 0; border-radius: 5px; overflow: hidden; }
.playerbar__cover .cover { border-radius: 5px; }

.playerbar__track { display: block; color: var(--text); }
.playerbar__track:hover { color: #fff; text-decoration: underline; }

.playerbar__artist a {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  color: var(--text-faint);
}
.playerbar__artist a:hover { color: var(--text); }
.playerbar__artist .avatar { flex: none; }

.playerbar__stars { flex: none; margin-left: 10px; }

@media (max-width: 900px) {
  /* The bar is already tight at this width; the stars are the first thing that
     can wait until you are on the track's own page. */
  .playerbar__stars { display: none; }
}
@media (max-width: 760px) {
  .trackpage { grid-template-columns: 1fr; }
  .trackpage__art { justify-self: center; }
  .trackpage__art .cover { width: 220px !important; height: 220px !important; }
}

/* Sign in, sign up and connecting an app all share the auth card above. */

/* ==================================================================== tips */

/* THE TIP IS THE ACCENT ACTION wherever it appears. It is the one control on a
   profile that moves money to a PERSON rather than to the house, so it carries
   the magenta instead of sitting in the grey ghost set with everything else.
   It was a btn--small: 12px of text adrift in a 42px box, next to a 24px pill
   of its own -- two boxes of different heights arguing with each other. Now
   there is one box, and what you have already given is quiet text beside it. */

.tip {
  display: inline-flex;
  align-items: center;
  gap: 14px;
  flex-wrap: wrap;
  margin: 18px 0 4px;
}
.tip--compact { margin: 0; }

.tip__go {
  border-radius: 999px;
  padding: 9px 20px;
  min-height: 38px;
  font-size: 14px;
  color: #ffd9f5;
  background: rgba(255, 5, 199, 0.10);
  border-color: rgba(255, 5, 199, 0.34);
}
.tip__go:hover:not(:disabled) {
  color: #fff !important;
  background: rgba(255, 5, 199, 0.20);
  border-color: rgba(255, 5, 199, 0.62);
  box-shadow: 0 0 26px -10px rgba(255, 5, 199, 0.95);
}
.tip__go:disabled { opacity: 0.6; cursor: default; }
.tip__go i { color: var(--neon-magenta); }
.tip__go:hover i { color: #ff5ad9; }

/* Quiet, because it is a fact about you rather than something to press. */
.tip__given {
  font-size: 13px;
  color: var(--text-faint);
  white-space: nowrap;
}
.tip__given strong { color: var(--neon-magenta); font-weight: 600; }

.tip__said { font-size: 13px; color: #ff9a9a; }
.tip__said.is-good { color: var(--neon-green); }

@media (max-width: 560px) {
  .tip { gap: 10px; }
  .tip__go { padding: 9px 16px; }
}

/* ================================================================== admin */

/* The admin panel used to be the one page that had missed every redesign: a
   purple slab, legacy grey/green buttons with black text, and inline styles on
   every input. It now uses the same shell, tabs, tables and buttons as the
   rest of the site, which is why almost all of this is layout rather than
   colour -- the colours already exist as tokens. */

/* The heading shares the tabs' measure. .container spans the full window on
   purpose -- the video behind it should too -- and everything inside the admin
   page is capped at --page and centred, so a heading left at the container's
   own width started a long way to the left of the panel it titles. */
.admhead, .admerror { max-width: var(--page); margin-inline: auto; }
.admhead { display: flex; align-items: baseline; gap: 14px; flex-wrap: wrap; margin-bottom: 4px; }
.admhead h1 { margin: 0; }
.admhead__who { font-size: 13px; color: var(--text-faint); }

/* The dashboard. Numbers first, because the question an admin arrives with is
   usually "is anything obviously wrong". */
.admstats {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
  gap: 12px;
  margin: 18px 0 8px;
}
.admstat {
  background: rgba(255, 255, 255, 0.035);
  border: 1px solid rgba(255, 255, 255, 0.09);
  border-radius: 12px;
  padding: 14px 16px;
}
.admstat__n { font-size: 26px; line-height: 1.1; font-weight: 600; }
.admstat__l { font-size: 12px; color: var(--text-faint); margin-top: 4px; }
.admstat--money .admstat__n { color: var(--neon-magenta); }
.admstat--warn .admstat__n { color: #ffb3b3; }

/* A section. Not a slab: a hairline card on the page ground, like everywhere
   else on the site. */
.admcard {
  background: rgba(255, 255, 255, 0.022);
  border: 1px solid rgba(255, 255, 255, 0.08);
  border-radius: 14px;
  padding: 18px 20px;
  margin-bottom: 20px;
}
.admcard > h3, .admcard > h4 { margin-top: 0; }
.admcard__note { font-size: 13px; color: var(--text-faint); margin-top: 0; }

.admbar { display: flex; gap: 8px; flex-wrap: wrap; align-items: center; margin: 12px 0; }
.admbar--end { justify-content: flex-end; }

/* Fields. One rule instead of the `input` style object that was spread onto
   every control by hand. */
.admfield { display: block; margin-bottom: 12px; }
.admfield > span { display: block; font-size: 12px; color: var(--text-faint); margin-bottom: 4px; }
.admfield input[type="text"], .admfield input[type="date"], .admfield input[type="number"],
.admfield select, .admfield textarea, .adminput {
  width: 100%;
  box-sizing: border-box;
  padding: 9px 11px;
  border: 1px solid rgba(255, 255, 255, 0.16);
  border-radius: 8px;
  background: rgba(0, 0, 0, 0.42);
  color: var(--text);
  font-size: 14px;
  font-family: inherit;
}
.admfield textarea { min-height: 120px; resize: vertical; }
.admfield input:focus, .admfield select:focus, .admfield textarea:focus, .adminput:focus {
  outline: none;
  border-color: rgba(90, 217, 255, 0.55);
  box-shadow: 0 0 0 3px rgba(90, 217, 255, 0.12);
}
.admcheck { display: flex; align-items: center; gap: 8px; margin-bottom: 12px; font-size: 14px; }
.admcheck input { width: 16px; height: 16px; }

/* Two or three fields on a line, one under 720px. */
.admgrid { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 0 16px; }

/* The panel that opens under a row. */
.admdrawer {
  margin-top: 16px;
  padding: 18px;
  background: rgba(0, 0, 0, 0.34);
  border: 1px solid rgba(255, 255, 255, 0.10);
  border-left: 3px solid var(--neon-cyan);
  border-radius: 10px;
}
.admdrawer h4 { margin: 0 0 4px; }
.admdrawer__sub { font-size: 13px; color: var(--text-faint); margin: 0 0 14px; }
.admdrawer__section { margin-top: 20px; padding-top: 16px; border-top: 1px solid rgba(255,255,255,0.08); }
.admdrawer__section h5 { margin: 0 0 6px; font-size: 14px; }

.admchip {
  display: inline-flex; align-items: center; gap: 6px;
  font-size: 12px; padding: 3px 10px; border-radius: 999px;
  border: 1px solid rgba(255, 255, 255, 0.16);
  background: rgba(255, 255, 255, 0.05);
  color: var(--text-faint);
  white-space: nowrap;
}
.admchip--on { border-color: rgba(80, 220, 140, 0.45); background: rgba(80, 220, 140, 0.10); color: #b6f5cf; }
.admchip--off { opacity: 0.6; }
.admchip--dead { border-color: rgba(255, 120, 120, 0.4); background: rgba(255, 70, 70, 0.10); color: #ffb3b3; }
.admchip button {
  background: none; border: 0; color: inherit; cursor: pointer;
  padding: 0; font-size: 12px; opacity: 0.7;
}
.admchip button:hover { opacity: 1; color: #fff; }

.admchips { display: flex; gap: 6px; flex-wrap: wrap; }

/* Tables: the shared table.table, plus admin-only affordances. */
.admtable { width: 100%; }
.admtable td, .admtable th { vertical-align: middle; }
.admtable tr.is-admin td { background: rgba(80, 220, 140, 0.07); }
.admtable tr.is-hidden { opacity: 0.5; }
.admtable .admtable__right { text-align: right; white-space: nowrap; }
.admnum { font-variant-numeric: tabular-nums; }
.admdim { color: var(--text-faint); }
.admflc { font-size: 11px; color: var(--neon-cyan); background: rgba(0,0,0,0.4); padding: 1px 5px; }

/* ============================================================== pulse */

/* One measure a row, three windows across it. A TABLE WOULD BE WRONG here:
   these are seven unrelated measures that happen to share three column
   headings, and ruling them into a grid invites reading down a column that
   adds visitors to purchases. */
.pulses { display: grid; gap: 2px; margin-bottom: 4px; }
.pulse {
  display: flex; align-items: baseline; flex-wrap: wrap; gap: 4px 18px;
  padding: 8px 10px; background: rgba(255, 255, 255, 0.02);
}
.pulse__l { flex: 0 0 190px; font-size: 13px; color: var(--text-dim); }
.pulse__note { color: var(--text-faint); font-size: 11px; }
.pulse__row { display: flex; flex-wrap: wrap; gap: 4px 22px; font-size: 12px; color: var(--text-faint); }
.pulse__row b { font-size: 17px; color: var(--text); margin-right: 5px; font-weight: 600; }

@media (max-width: 700px) {
  .pulse__l { flex-basis: 100%; }
}

/* ============================================================== charts */

/* SINGLE SERIES, ONE HUE. Length carries magnitude; the colour never does, so
   there is no palette here that a filter can shuffle. Text wears text tokens
   throughout -- the green means "this is data", and must not also mean "this
   is a word". See components/Charts.tsx. */
.chart { margin: 14px 0 18px; }
.chart__head { display: flex; align-items: baseline; justify-content: space-between; gap: 12px; }
.chart__peak { font-size: 12px; color: var(--text-faint); }
.chart__toggle {
  font-size: 11px; color: var(--text-faint); background: none; border: 0;
  cursor: pointer; padding: 2px 4px; text-decoration: underline;
}
.chart__toggle:hover { color: var(--text-dim); }

.chart__plot { position: relative; margin-top: 8px; }

/* Hairline, solid, one step off the surface. Never dashed: a dashed rule is
   more ink than a solid one and reads as data. */
.chart__grid {
  position: absolute; left: 0; right: 0; height: 0;
  border-top: 1px solid var(--border);
}
.chart__grid span {
  position: absolute; right: 0; top: -8px; font-size: 10px;
  color: var(--text-faint);
  /* Knocks out the bar behind it. The admin card is a near-transparent
     white over the page, not --panel, so the label matches the card rather
     than a surface it is not sitting on. */
  background: #1a1a20; padding: 0 4px;
}

/* The 2px gap IS the separator -- surface showing through, not a stroke drawn
   around each bar, which would add ink that is not data. */
.chart__bars { position: absolute; inset: 0; display: flex; align-items: flex-end; gap: 2px; }
.chart__slot { flex: 1; height: 100%; display: flex; align-items: flex-end; min-width: 0; cursor: default; }
.chart__bar {
  width: 100%; max-width: 24px; margin: 0 auto;
  background: var(--green);
  /* Rounded at the data end, square at the baseline: the flat foot is what
     says these all start from the same place. */
  border-radius: 4px 4px 0 0;
  min-height: 1px;
  transition: opacity 120ms;
}
.chart__slot:hover .chart__bar, .chart__bar.is-at { opacity: 0.75; }

.chart__foot {
  display: flex; justify-content: space-between; gap: 12px;
  margin-top: 6px; font-size: 11px; color: var(--text-faint);
}
/* The hovered value replaces the axis ends rather than floating over the bar
   it describes. */
.chart__at { color: var(--text-dim); }
.chart__at b { color: var(--text); }

.chart__table { margin-top: 8px; font-size: 13px; }
.chart__picker { display: flex; flex-wrap: wrap; gap: 6px; margin: 12px 0 4px; }

.spark { display: block; overflow: visible; }
.spark__line { stroke: var(--green); }
/* The ring is the surface colour, so the dot stays legible where it sits on
   the line it ends. */
.spark__end { fill: var(--green); stroke: var(--panel); }
.admin-neg { color: #ffb3b3; }
.admin-pos { color: var(--neon-green); }

.admpager { display: flex; align-items: center; gap: 12px; margin-top: 10px; font-size: 13px; }

.admcode {
  display: block; word-break: break-all; font-size: 12px;
  color: var(--neon-cyan); background: rgba(0,0,0,0.5);
  padding: 10px; border-radius: 8px; margin: 6px 0 0;
}

.admerror {
  border: 1px solid rgba(255, 120, 120, 0.45);
  background: rgba(255, 70, 70, 0.12);
  color: #ffd9d9;
  border-radius: 10px;
  padding: 12px 16px;
  margin-bottom: 16px;
}

@media (max-width: 720px) {
  .admcard { padding: 14px; }
  .admstat__n { font-size: 22px; }
}

/* ================================================== signing in and signing up */

/*
 * One card, used by sign in, sign up and connecting an app.
 *
 * What was here before was the legacy Blazor layout, and it was broken rather
 * than merely dated. `.middlechild` is position:fixed with translate(0,-50%),
 * so the form was taken out of the flow entirely: the footer laid itself out as
 * though the page were empty and the form painted on top of it. That is the
 * overlap in the screenshots, and it got worse the more the page was scaled,
 * because a fixed element cannot grow the document it is no longer part of.
 *
 * Everything below is ordinary flow. It is a single column at every width --
 * the old two-column split put the prose beside the fields and then dropped the
 * fields below the fold on a phone -- and every size is clamped or relative, so
 * a browser at 200% gets a bigger card rather than a broken one.
 */
.auth {
  /* A gutter on a phone: edge-to-edge, the card's own border sits half off the
     screen and it stops reading as a card at all. */
  width: min(460px, calc(100% - 28px));
  box-sizing: border-box;
  margin: clamp(24px, 6vw, 72px) auto clamp(40px, 9vw, 96px);
  padding: clamp(20px, 4vw, 34px);
  background: var(--panel-a);
  border: 1px solid var(--border);
  border-radius: var(--radius);
}

.auth__kicker {
  font-size: 11px;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--text-faint);
  margin-bottom: 8px;
}

.auth h1 {
  margin: 0 0 8px;
  font-size: clamp(26px, 5vw, 34px);
  line-height: 1.1;
}

.auth__lede {
  color: var(--text-dim);
  font-size: 15px;
  line-height: 1.55;
  margin: 0 0 22px;
}

.auth__form { display: grid; gap: 16px; }

.auth__field { display: grid; gap: 6px; }
.auth__field > span {
  font-size: 13px;
  color: var(--text-dim);
  letter-spacing: 0.02em;
}
/* 16px so iOS does not zoom the whole page in when the field is focused, which
   on a narrow screen leaves the user scrolled sideways into nothing. */
.auth__field input { font-size: 16px; min-height: 46px; }

/* A fault with one field, under that field, where the thing it is about is. */
.auth__fieldnote {
  font-size: 13px;
  color: var(--brand-soft);
  margin: 0;
}

/*
 * A failed attempt. It reads as part of the form now: the old one was a
 * full-bleed red slab with an <h1> in it, above the page, which pushed the form
 * off screen at the exact moment somebody needed to correct it -- the second
 * screenshot is the whole viewport filled with "Username and password are
 * required" and no visible username box.
 */
.auth__error {
  display: flex;
  align-items: flex-start;
  gap: 10px;
  padding: 11px 13px;
  border: 1px solid rgba(255, 107, 107, 0.42);
  border-left-width: 3px;
  border-radius: var(--radius);
  background: rgba(255, 70, 70, 0.11);
  color: #ffc9c9;
  font-size: 14px;
  line-height: 1.45;
}
.auth__error i { color: #ff6b6b; margin-top: 2px; }

/* Full width, and tall enough to hit with a thumb. */
.auth__go {
  width: 100%;
  min-height: 48px;
  font-size: 15px;
  justify-content: center;
}

.auth__alt {
  margin: 18px 0 0;
  padding-top: 16px;
  border-top: 1px solid rgba(255, 255, 255, 0.08);
  font-size: 14px;
  color: var(--text-faint);
  display: flex;
  flex-wrap: wrap;
  gap: 6px 14px;
  justify-content: space-between;
}
.auth__alt a { color: var(--blue); }

/* Connecting an app is the same card with a result state on the end of it. */
.auth--wide { width: min(520px, calc(100% - 28px)); text-align: center; }

/*
 * The popup. No nav, no footer, no background video, so the card centres itself
 * in the window rather than sitting under a page that is not there.
 *
 * It has to survive being a TAB rather than a window -- Firefox turns
 * window.open into a tab by default and any browser may -- so this is centring
 * and min-height rather than anything that assumes 460 by 640. Full width it is
 * the same card with more room around it.
 */
.auth--bare {
  margin: 0 auto;
  min-height: 100svh;
  display: flex;
  flex-direction: column;
  justify-content: center;
  border: 0;
  background: none;
}
.auth--wide .auth__lede { max-width: 42ch; margin-left: auto; margin-right: auto; }
.auth--wide .auth__form { text-align: left; }

.auth__tick {
  width: clamp(58px, 12vw, 74px);
  aspect-ratio: 1;
  margin: 0 auto 18px;
  border-radius: 50%;
  display: grid;
  place-items: center;
  font-size: clamp(24px, 5vw, 30px);
  color: #04140a;
  background: linear-gradient(135deg, var(--neon-green), #12c9a0);
  box-shadow: 0 0 44px -8px rgba(39, 248, 54, 0.7);
}

.auth__hint { font-size: 13px; color: var(--text-faint); margin: 16px 0 0; }
.auth__os { font-size: 13px; color: var(--text-faint); margin: 0; }

/* ============================================================= a dvd case */

/*
 * A real box, not a picture of one: six faces in a preserve-3d context at the
 * proportions of a DVD keep case -- 190 x 135 x 14 mm, so the depth is 10.4% of
 * the width. See DvdBox.tsx for why this is CSS and not a 3D model.
 *
 * TURNED TOWARD ITS SPINE, the way a case stands on a shelf. The spine carries
 * the one marking that says what kind of thing this is, so it has to be the
 * edge you can see.
 *
 * --w, --h and --zoom come from the component, so everything below is derived
 * and the case is correct at any size from the 116px add-on tile to the 257px
 * one on the History page.
 */
.dvd {
  --d: calc(var(--w) * 0.104);          /* 14mm of depth */
  --rim: calc(var(--w) * 0.026);        /* the moulded lip around the sleeve */
  --r: calc(var(--w) * 0.032);          /* the corner radius, ~4mm */

  --plastic: hsl(var(--ph) 54% 15%);
  --plastic-lit: hsl(var(--ph) 40% 31%);
  --plastic-dark: hsl(var(--ph) 58% 7%);

  position: relative;
  display: inline-block;
  perspective: calc(var(--w) * 3.4);
  /* Centred, because the resting pose is now square-on and an off-centre
     vanishing point would keystone a face that is supposed to be flat. */
  perspective-origin: 50% 50%;
}

/* One hue of plastic per product type. There used to be a marking on the spine
   to go with it -- a bar for a product, ticks for a pack -- and it went: the
   label is printed in the cover's colour and carries a title, a code and a
   logo, and a decorative stripe down the edge of all that was just clutter. The
   plastic still says what type a thing is. */
.dvd--product { --ph: 282; }            /* the purple products always had */
.dvd--pack    { --ph: 122; }            /* the green loop packs always had */
.dvd--module  { --ph: 202; }            /* new: studio modules were green too */
.dvd--support { --ph: 32; }             /* new: so were supports */

/* Lifted over its neighbours while it is the one being looked at, or the zoom
   slides under the next tile along. */
.dvd:hover { z-index: 3; }

.dvd__box {
  position: absolute;
  inset: 0;
  transform-style: preserve-3d;
  transform:
    scale3d(var(--z, 1), var(--z, 1), var(--z, 1))
    rotateX(var(--rx, 0deg))
    rotateY(var(--ry, 0deg));
  /* Slow and eased on the way back to rest, quick while the cursor is driving
     it -- a long ease there would lag the pointer and feel like treacle. */
  transition: transform 460ms cubic-bezier(0.22, 0.61, 0.36, 1);
}
.dvd:hover .dvd__box { --z: var(--zoom, 1); transition: transform 110ms linear; }

/*
 * Turns itself square to the reader as the page arrives -- ONCE, and then the
 * class is gone for good (DvdBox drops it on animationend, or on the first
 * movement of a cursor over the case).
 *
 * It is a class rather than a plain rule on .dvd__box because a CSS animation
 * outranks the transform the cursor drives, so it had to be switched off while
 * hovering -- and `animation: none` under :hover meant the animation came BACK
 * when the cursor left, which restarted it. Leaving a case snapped it round to
 * 30 degrees and played the whole settle again instead of easing home from
 * where it actually was.
 */
.dvd__box.is-settling { animation: dvd-settle 1s cubic-bezier(0.22, 0.61, 0.36, 1); }

/*
 * The case shows itself on the way in: it arrives turned to its spine, the way
 * it sits on a shelf, and over a second squares up to the reader.
 *
 * ONLY A `from`. The implicit `to` is the element's own base transform, so the
 * animation always lands exactly where the resting state is rather than on a
 * duplicate of it that could drift out of step with it later. The base carries
 * the scale, so `from` states it too -- interpolating between transform lists of
 * different lengths falls back to matrix maths, which is a different curve.
 */
@keyframes dvd-settle {
  from { transform: scale3d(1, 1, 1) rotateX(2.5deg) rotateY(30deg); }
}

.dvd__face {
  position: absolute;
  top: 50%;
  left: 50%;
  backface-visibility: hidden;
}

/* Every face is centred first, then pushed out to its own plane -- the standard
   way to build a box, and the only one where the maths stays readable. */
.dvd__front, .dvd__back { width: var(--w); height: var(--h); }
.dvd__spine, .dvd__lip { width: var(--d); height: var(--h); }
.dvd__edge { width: var(--w); height: var(--d); }

.dvd__front {
  transform: translate(-50%, -50%) translateZ(calc(var(--d) / 2));
  background: var(--plastic);
  border-radius: var(--r);
  padding: var(--rim);
  box-sizing: border-box;
  /* The lip catches light along the top-left and loses it at the bottom-right,
     which is what stops the border reading as a flat coloured frame. */
  box-shadow:
    inset 1px 1px 0 hsl(var(--ph) 35% 42% / 0.85),
    inset -1px -1px 0 var(--plastic-dark);
}

.dvd__art {
  width: 100%;
  height: 100%;
  /* Stretched, not cropped -- what the original did, and the right call for box
     art: the covers are not all 1.4, and "cover" ate the last word of the
     Discord pack's title. A slightly squashed cover still reads; a cropped one
     loses the thing it says. */
  background-size: 100% 100%;
  border-radius: calc(var(--r) * 0.4);
  /* The sleeve sits UNDER the moulded lip, so the lip shades its edges. */
  box-shadow: inset 0 0 calc(var(--w) * 0.03) rgba(0, 0, 0, 0.55);
}

/* No artwork: a plain sleeve with the name on it, rather than an empty case
   that reads as a failed image. */
.dvd__art--blank {
  background: linear-gradient(160deg, #2b1240 0%, #4a1d6d 55%, #1a0a29 100%);
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 8%;
  box-sizing: border-box;
}
.dvd__notitle {
  color: rgba(255, 255, 255, 0.92);
  text-align: center;
  line-height: 1.25;
  font-size: calc(var(--w) * 0.105);
  overflow-wrap: anywhere;
}

/* The clear sleeve. One broad highlight that slides across as the case turns:
   --sheen runs about -1 to 1 under the cursor and is 0 at rest, so the light
   tracks the angle instead of sitting still the way the old photograph did. */
.dvd__gloss {
  position: absolute;
  inset: 0;
  border-radius: var(--r);
  pointer-events: none;
  background:
    linear-gradient(78deg,
      rgba(255, 255, 255, 0) 30%,
      rgba(255, 255, 255, 0.16) 43%,
      rgba(255, 255, 255, 0.05) 50%,
      rgba(255, 255, 255, 0) 60%),
    linear-gradient(160deg, rgba(255, 255, 255, 0.07), rgba(255, 255, 255, 0) 42%);
  background-size: 260% 100%, 100% 100%;
  background-position: calc(50% + var(--sheen, 0) * 40%) 0, 0 0;
  transition: background-position 110ms linear;
}

/* --------------------------------------------------------------- the spine */

/*
 * Laid out the way a game's spine is, and for the same reason: every part in
 * the same place on every case, so a shelf of them scans as a set. Top to
 * bottom -- the publisher's band, the title, the product's code, the
 * publisher's logo -- with two moulded hinge caps at the very ends.
 *
 * --label is the colour the label is PRINTED, taken from the cover art
 * (NGProducts.SpineColour). It falls back to the plastic, which is what every
 * case looked like before there was a column for it. --ink-on-label is black or
 * white, whichever can be read on that colour; see onLabel() in DvdBox.tsx.
 */
.dvd__spine {
  --label: var(--plastic);
  --ink-on-label: #ffffff;

  transform: translate(-50%, -50%) rotateY(-90deg) translateZ(calc(var(--w) / 2));
  background:
    /* The moulded caps. SMALL: on a real case these are a few millimetres of
       hinge, and the printed label runs almost the whole height between them. */
    linear-gradient(to bottom,
      hsl(var(--ph) 28% 10%) 0 3.5%,
      transparent 3.5% 96.5%,
      hsl(var(--ph) 28% 10%) 96.5%),
    /* The light across the printed card. TO LEFT, not to right: a rotated
       face's local axes are not the screen's, and lit-first put the highlight
       on the far edge -- which read as a black gap between the spine and the
       cover rather than as a corner. */
    linear-gradient(to left,
      rgba(255, 255, 255, 0.14), rgba(0, 0, 0, 0) 45%, rgba(0, 0, 0, 0.45)),
    var(--label);
  border-radius: calc(var(--r) * 0.5);
  overflow: hidden;
}

/*
 * The publisher's band at the head of the spine, where a console's would be:
 * "ezmuze" in the wordmark's own typeface on a dark plate, identical on every
 * case whatever the label underneath is printed in.
 */
.dvd__brand {
  position: absolute;
  top: 3.5%;
  left: 0;
  right: 0;
  height: 15%;
  display: flex;
  align-items: center;
  justify-content: center;
  background: rgba(0, 0, 0, 0.72);
  writing-mode: vertical-rl;
  text-orientation: mixed;
  font-family: Comfortaa, system-ui, sans-serif;
  font-weight: 700;
  font-size: calc(var(--d) * 0.34);
  letter-spacing: 0.12em;
  color: #fff;
  white-space: nowrap;
  overflow: hidden;
}

/*
 * The title, reading top to bottom the way a spine does on a shelf.
 *
 * writing-mode rather than a rotate(90deg): it gives the element real vertical
 * metrics, so an ellipsis on a name too long for the case works, where a
 * rotated box would have been measured horizontally and clipped at the wrong
 * end. The component leaves this out below about 180px wide, where the spine is
 * under 19px and the type would be a smear rather than a word.
 */
.dvd__spinetext {
  position: absolute;
  top: 20%;
  bottom: 26%;
  left: 0;
  right: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  writing-mode: vertical-rl;
  text-orientation: mixed;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  font-size: calc(var(--d) * 0.4);
  letter-spacing: 0.02em;
  color: var(--ink-on-label);
  opacity: 0.94;
}

/* The four-letter code, sitting just above the logo the way a catalogue number
   does on a real spine. Small, and set apart from the title. */
.dvd__code {
  position: absolute;
  bottom: 18%;
  left: 0;
  right: 0;
  height: 8%;
  display: flex;
  align-items: center;
  justify-content: center;
  writing-mode: vertical-rl;
  text-orientation: mixed;
  font-size: calc(var(--d) * 0.28);
  letter-spacing: 0.14em;
  color: var(--ink-on-label);
  opacity: 0.72;
  white-space: nowrap;
  overflow: hidden;
}

/*
 * The publisher's logo at the foot.
 *
 * mad ninja skillz, and the two marks are not interchangeable: everything in
 * the catalogue was published under the classic one, and ezmuze studio -- the
 * only thing made since -- carries the current one. Both are drawn on their own
 * black ground, so they sit on a plate rather than being knocked out of the
 * label, which is also what keeps them legible on ninja dnb's yellow.
 */
.dvd__pub {
  position: absolute;
  bottom: 5%;
  left: 50%;
  transform: translateX(-50%);
  width: 72%;
  aspect-ratio: 1;
  border-radius: 16%;
  background: center / cover no-repeat;
  box-shadow: 0 0 0 1px rgba(255, 255, 255, 0.18);
}
/* An author's own picture, where there is one. ROUND, because it is a face and
   every other picture of a person on this site is round -- and because at spine
   size the shape is what tells a person apart from a logo, long before either
   is legible. The rim is a touch brighter than the logos get: a photograph has
   no flat edge of its own to sit against the plastic. */
.dvd__pub--author {
  border-radius: 50%;
  box-shadow: 0 0 0 1px rgba(255, 255, 255, 0.32);
}

.dvd__pub--classic { background-image: url('/img/madninja-classic.png'); }
.dvd__pub--new     { background-image: url('/img/madninja.png'); }
.dvd__pub--xbox    { background-image: url('/img/xbox360.png'); }

/* The opening edge: the two halves of the case meet here, so it is a pair of
   lips with a dark seam between them rather than one flat strip. */
.dvd__lip {
  transform: translate(-50%, -50%) rotateY(90deg) translateZ(calc(var(--w) / 2));
  background: linear-gradient(to right,
    var(--plastic-dark), var(--plastic) 24%,
    hsl(var(--ph) 60% 4%) 48% 52%,
    var(--plastic) 76%, var(--plastic-dark));
  border-radius: calc(var(--r) * 0.5);
}

.dvd__back {
  transform: translate(-50%, -50%) translateZ(calc(var(--d) / -2)) rotateY(180deg);
  background: linear-gradient(200deg, var(--plastic), var(--plastic-dark));
  border-radius: var(--r);
}

/* The two short edges. */
.dvd__top {
  transform: translate(-50%, -50%) rotateX(90deg) translateZ(calc(var(--h) / 2));
  background: linear-gradient(to bottom, var(--plastic-lit), var(--plastic) 45%, var(--plastic-dark));
}
.dvd__bottom {
  transform: translate(-50%, -50%) rotateX(-90deg) translateZ(calc(var(--h) / 2));
  background: linear-gradient(to top, var(--plastic-dark), var(--plastic) 60%);
}

/* Grounds the box. Slides the opposite way to the tilt, so the case looks like
   it is being turned over a surface rather than painted onto one. */
.dvd__shadow {
  position: absolute;
  left: 6%;
  right: 6%;
  bottom: calc(var(--w) * -0.045);
  height: calc(var(--w) * 0.09);
  border-radius: 50%;
  background: radial-gradient(ellipse at center, rgba(0, 0, 0, 0.55), rgba(0, 0, 0, 0) 70%);
  filter: blur(calc(var(--w) * 0.012));
  transform: translateX(calc(var(--sheen, 0) * var(--w) * -0.05));
  transition: transform 460ms cubic-bezier(0.22, 0.61, 0.36, 1), opacity 460ms ease;
  pointer-events: none;
}
.dvd:hover .dvd__shadow {
  opacity: 0.7;
  transition: transform 110ms linear, opacity 110ms ease;
}

/* Somebody who has asked for less movement still gets the box, the colour and
   the spine -- just a still one: the geometry is the point, the turning and the
   zoom are the flourish. */
@media (prefers-reduced-motion: reduce) {
  .dvd__box, .dvd:hover .dvd__box, .dvd__gloss,
  .dvd__shadow, .dvd:hover .dvd__shadow { transition: none; }
  .dvd:hover .dvd__box { --z: 1; }
  /* Square-on from the first frame: the settle is the flourish. */
  .dvd__box.is-settling { animation: none; }
}


/* ============================================================ a cd jewel case

   The same object as the keep case above and built the same way -- six faces in
   a preserve-3d context, --w/--h/--zoom from the component, everything else
   derived -- at the proportions of a jewel case: 142 x 125 x 10 mm, so the
   depth is 8% of the width against the keep case's 10.4%.

   WHAT MAKES IT READ AS A CD, and the reason this is not a variant of .dvd: the
   booklet is SQUARE (120 x 120 in a 125 x 142 case) and floats in a taller
   frame, so there is a band of bare plastic above and below the art. That band
   is the whole silhouette. A keep case's sleeve fills its front edge to edge
   and is stretched to 1.4 to do it; nothing about the two shapes is shared
   except the technique.

   The plastic is neutral rather than one hue per kind. A keep case's colour
   says which product type it is -- there are four, and a shelf of them sorts by
   eye. A record has no type: what distinguishes one from another is the art and
   whose name is on it, so tinting the plastic would be decoration pretending to
   be information. The SPINE takes a tint (--label) where a cover's colour is
   known, exactly as the keep case's does. */

.cd {
  --d: calc(var(--w) * 0.08);           /* 10mm of depth */
  --rim: calc(var(--w) * 0.02);         /* the lid's lip around the booklet */
  --r: calc(var(--w) * 0.022);          /* the corner radius, ~3mm */

  /* Smoked, near-black plastic: the tray half of a jewel case, which is the
     part you actually see behind and around the booklet. */
  --cdplastic:      hsl(258 12% 13%);
  --cdplastic-lit:  hsl(258 10% 27%);
  --cdplastic-dark: hsl(258 14% 6%);

  position: relative;
  display: inline-block;
  perspective: calc(var(--w) * 3.4);
  perspective-origin: 50% 50%;
  line-height: 0;
}

.cd:hover { z-index: 3; }

.cd__box {
  position: absolute;
  inset: 0;
  transform-style: preserve-3d;
  transform:
    scale3d(var(--z, 1), var(--z, 1), var(--z, 1))
    rotateX(var(--rx, 0deg))
    rotateY(var(--ry, 0deg));
  transition: transform 460ms cubic-bezier(0.22, 0.61, 0.36, 1);
}
.cd:hover .cd__box { --z: var(--zoom, 1); transition: transform 110ms linear; }

/* Once, on arrival, then the class is dropped for good -- see the keep case's
   note for why this cannot be a plain rule on the element. */
.cd__box.is-settling { animation: cd-settle 1s cubic-bezier(0.22, 0.61, 0.36, 1); }

/* Only a `from`: the implicit `to` is the element's own base transform, so it
   always lands exactly on the resting state rather than on a copy of it that
   could drift. 34deg rather than the keep case's 30 -- a 10mm spine needs the
   extra turn to read as a face rather than an edge. */
@keyframes cd-settle {
  from { transform: scale3d(1, 1, 1) rotateX(2.5deg) rotateY(34deg); }
}

.cd__face {
  position: absolute;
  top: 50%;
  left: 50%;
  backface-visibility: hidden;
}

.cd__front, .cd__back { width: var(--w); height: var(--h); }
.cd__spine, .cd__lip { width: var(--d); height: var(--h); }
.cd__edge { width: var(--w); height: var(--d); }

/* The lid: hard, glassy and a shade lighter than the tray behind it. */
.cd__front {
  transform: translate(-50%, -50%) translateZ(calc(var(--d) / 2));
  background: linear-gradient(155deg, hsl(258 10% 20%), hsl(258 12% 11%));
  border-radius: var(--r);
  box-sizing: border-box;
  /* The lid's lip, and the only thing between the art and the edge now. */
  padding: var(--rim);
  display: flex;
  align-items: center;
  justify-content: center;
  box-shadow:
    inset 1px 1px 0 hsl(258 12% 34% / 0.9),
    inset -1px -1px 0 var(--cdplastic-dark);
}

/*
 * The booklet, FILLING THE FRONT. It began as the faithful thing -- a square
 * 120mm insert floating in a 125 x 142 case, with bands of bare plastic above
 * and below -- and that was the wrong trade: the cover is what people came to
 * look at, and the moulding was eating about 15% of it to be true to a piece of
 * 1990s packaging. The case takes its shape from the artwork now (see CdCase),
 * so there is nothing left over to frame.
 *
 * Inset by the lid's lip only, which is what keeps it reading as something
 * BEHIND plastic rather than a picture stuck on the front.
 */
.cd__art {
  position: relative;
  width: 100%;
  height: 100%;
  overflow: hidden;
  border-radius: calc(var(--r) * 0.35);
  background: #0b0b10;
  /* The booklet sits UNDER the lid's lip, so its edges are shaded by it. */
  box-shadow: inset 0 0 calc(var(--w) * 0.03) rgba(0, 0, 0, 0.6);
  display: flex;
  align-items: center;
  justify-content: center;
}
/* Whatever the caller put in the slot -- an <img>, or the generated CoverArt
   the rest of the vault draws -- fills it. Cover, not stretch: album art is
   square already, so there is nothing to squash and a stray non-square upload
   should crop rather than distort. */
.cd__art img,
.cd__art > * { width: 100%; height: 100%; object-fit: cover; display: block; }

.cd__art--blank {
  background: linear-gradient(160deg, #16233a 0%, #23365c 55%, #0d1424 100%);
  padding: 8%;
  box-sizing: border-box;
}
.cd__notitle {
  color: rgba(255, 255, 255, 0.92);
  text-align: center;
  line-height: 1.25;
  font-size: calc(var(--w) * 0.1);
  overflow-wrap: anywhere;
  /* Overrides the fill above: this is type, not artwork. */
  width: auto !important; height: auto !important;
}

/* The clear lid. Harder and narrower than the keep case's sleeve -- a jewel
   case is rigid polystyrene and catches a tighter, brighter line than a soft
   wallet does. */
.cd__gloss {
  position: absolute;
  inset: 0;
  border-radius: var(--r);
  pointer-events: none;
  background:
    linear-gradient(80deg,
      rgba(255, 255, 255, 0) 34%,
      rgba(255, 255, 255, 0.22) 45%,
      rgba(255, 255, 255, 0.06) 51%,
      rgba(255, 255, 255, 0) 62%),
    linear-gradient(160deg, rgba(255, 255, 255, 0.09), rgba(255, 255, 255, 0) 40%);
  background-size: 260% 100%, 100% 100%;
  background-position: calc(50% + var(--sheen, 0) * 40%) 0, 0 0;
  transition: background-position 110ms linear;
}

/* ------------------------------------------------------------ the hinge side */

/*
 * NOT a flat printed card like a keep case's spine. The lid is hinged along
 * this edge, so a real one is two moulded posts at the ends with the booklet's
 * printed spine showing between them -- and those two teeth are most of what
 * makes the shape recognisable across a room. They are drawn at every size,
 * including the ones where the text is dropped.
 */
.cd__spine {
  --label: var(--cdplastic);
  --ink-on-label: #ffffff;

  transform: translate(-50%, -50%) rotateY(-90deg) translateZ(calc(var(--w) / 2));
  background:
    linear-gradient(to left,
      rgba(255, 255, 255, 0.16), rgba(0, 0, 0, 0) 45%, rgba(0, 0, 0, 0.5)),
    var(--label);
  border-radius: calc(var(--r) * 0.5);
  overflow: hidden;
}

/* The two hinge posts. Taller than the keep case's moulded caps -- on a jewel
   case they are a real part of the object rather than a few millimetres of
   trim, and they are opaque plastic whatever the spine is printed in. */
.cd__hinge {
  position: absolute;
  left: 0;
  right: 0;
  height: 11%;
  background: linear-gradient(to left, hsl(258 10% 24%), hsl(258 14% 8%));
}
.cd__hinge--top { top: 0; box-shadow: 0 1px 0 rgba(0, 0, 0, 0.6); }
.cd__hinge--bottom { bottom: 0; box-shadow: 0 -1px 0 rgba(0, 0, 0, 0.6); }

/*
 * The album, reading top to bottom the way a spine does on a shelf.
 *
 * writing-mode rather than a rotate(90deg), for the reason the keep case gives:
 * it gives the element real vertical metrics, so an ellipsis on a name too long
 * for the case works -- a rotated box is measured horizontally and clips at the
 * wrong end.
 */
.cd__spinetext {
  position: absolute;
  top: 11%;
  /* Its own band. These two used to share a box -- the title centred in it and
     the artist pinned to the end -- which is fine until an album is called "The
     Stars Are The Only Things Tying Us Together" and runs into the person who
     made it. */
  bottom: 44%;
  left: 0;
  right: 0;
  display: flex;
  align-items: center;
  justify-content: flex-start;
  writing-mode: vertical-rl;
  text-orientation: mixed;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  font-size: calc(var(--d) * 0.55);
  letter-spacing: 0.02em;
  color: var(--ink-on-label);
  opacity: 0.95;
}

/* Whose record it is, under the title. A record is BY somebody, and on a shelf
   that is the second thing you look for after the name -- which is why the
   spine carries it where the keep case carries a catalogue code. */
.cd__spineby {
  position: absolute;
  top: 58%;
  bottom: 26%;
  left: 0;
  right: 0;
  display: flex;
  align-items: flex-end;
  justify-content: flex-start;
  writing-mode: vertical-rl;
  text-orientation: mixed;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  font-size: calc(var(--d) * 0.4);
  color: var(--ink-on-label);
  opacity: 0.68;
}

/* The artist at the foot, where a label's mark goes on a real spine. Round, the
   same rule as the keep case's author mark: at spine size the shape is what
   separates a person from a logo long before either is legible. */
.cd__mark {
  position: absolute;
  bottom: 13.5%;
  left: 50%;
  transform: translateX(-50%);
  width: 66%;
  aspect-ratio: 1;
  border-radius: 50%;
  background: center / cover no-repeat;
  box-shadow: 0 0 0 1px rgba(255, 255, 255, 0.32);
}

/* ------------------------------------------------------------- the far side */

.cd__lip {
  transform: translate(-50%, -50%) rotateY(90deg) translateZ(calc(var(--w) / 2));
  /* One seam, not two: a jewel case opens on this edge as a single lid rather
     than a keep case's two folding halves. */
  background: linear-gradient(to right,
    var(--cdplastic-dark), var(--cdplastic) 40%,
    hsl(258 14% 4%) 49% 51%,
    var(--cdplastic) 62%, var(--cdplastic-dark));
  border-radius: calc(var(--r) * 0.5);
}

.cd__back {
  transform: translate(-50%, -50%) translateZ(calc(var(--d) / -2)) rotateY(180deg);
  background: linear-gradient(200deg, var(--cdplastic), var(--cdplastic-dark));
  border-radius: var(--r);
}

.cd__top {
  transform: translate(-50%, -50%) rotateX(90deg) translateZ(calc(var(--h) / 2));
  background: linear-gradient(to bottom, var(--cdplastic-lit), var(--cdplastic));
  border-radius: calc(var(--r) * 0.5);
}
.cd__bottom {
  transform: translate(-50%, -50%) rotateX(-90deg) translateZ(calc(var(--h) / 2));
  background: linear-gradient(to top, var(--cdplastic-dark), var(--cdplastic));
  border-radius: calc(var(--r) * 0.5);
}

/* Sits under the case rather than on it: a separate element outside the 3D
   context, so it does not turn with the box. */
.cd__shadow {
  position: absolute;
  left: 8%;
  right: 8%;
  bottom: -4%;
  height: 8%;
  border-radius: 50%;
  background: radial-gradient(50% 50% at 50% 50%, rgba(0, 0, 0, 0.6), rgba(0, 0, 0, 0));
  filter: blur(calc(var(--w) * 0.02));
  pointer-events: none;
}

/* ==========================================================================
   /comingsoon — the landing page ezmuze.studio points at until launch.

   One job: the name, the box, and the date. Anyone here followed a link for
   the application itself, so the page owes them the date and nothing to wade
   through. It borrows the hero's two-column shape rather than inventing a
   second one, so the box sits exactly where the box sits on the home page.
   ========================================================================== */
.coming {
  display: flex;
  align-items: center;
  justify-content: center;
  min-height: min(72vh, 720px);
  padding: calc(var(--u) * 3) 0;
}

.coming__inner {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, 0.9fr);
  gap: clamp(24px, 4vw, 56px);
  align-items: center;
  max-width: var(--page);
  width: 100%;
  margin: 0 auto;
}

.coming__title {
  font-size: clamp(34px, 5vw, 58px);
  margin: 0 0 var(--u);
}

.coming__lead {
  font-size: clamp(16px, 1.6vw, 19px);
  color: var(--text-dim);
}

.coming__when {
  font-size: clamp(18px, 1.8vw, 22px);
  color: var(--brand-soft);
  margin-top: calc(var(--u) * 1.5);
  margin-bottom: var(--u);
}

.coming__live {
  font-size: clamp(20px, 2.2vw, 26px);
  color: var(--brand-soft);
  margin-top: calc(var(--u) * 1.5);
}

.coming__case { display: flex; justify-content: center; }

/* The four units read as one clock: a shared baseline, tabular figures so the
   seconds do not shove their neighbours about as they tick, and the label
   under the number rather than beside it so a narrow screen keeps all four in
   a row instead of wrapping mid-count. */
.countdown {
  display: flex;
  gap: clamp(10px, 2vw, 22px);
  flex-wrap: nowrap;
}

.countdown__unit {
  min-width: 62px;
  padding: calc(var(--u) * 0.75) var(--u);
  border-radius: 10px;
  background: var(--panel-a);
  text-align: center;
}

.countdown__value {
  font-size: clamp(24px, 3.4vw, 40px);
  font-weight: 600;
  line-height: 1.1;
  font-variant-numeric: tabular-nums;
  font-feature-settings: 'tnum' 1;
}

.countdown__label {
  font-size: 12px;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--text-faint);
  margin-top: 4px;
}

/* One column, and the box goes first: on a phone the artwork is the thing
   worth leading with, and the countdown still lands above the fold under it. */
@media (max-width: 900px) {
  .coming__inner { grid-template-columns: minmax(0, 1fr); }
  .coming__case  { order: -1; }
  .coming        { min-height: 0; }
}

/* ==================================================== beta access, /betaAccess

   A DOCUMENT, so it is measured like one: the same 74ch reading column the
   legal pages use, centred, rather than a 1220px page with body copy pinned
   down its left edge and a thousand pixels of nothing beside it. Everything
   on the page -- the rules between sections, the two report cards, the shelf
   of packs -- is sized to that column, which is what makes it read as one
   thing rather than as text with furniture around it. */

.beta {
  max-width: 74ch;
  margin: clamp(20px, 3vw, 34px) auto 0;
  padding-bottom: 48px;
}
.beta p, .beta li { line-height: 1.7; }

.beta__head { padding: 8px 0 26px; }
.beta__kicker {
  font-size: 12px; letter-spacing: 0.14em; text-transform: uppercase;
  color: var(--neon-green); margin-bottom: 10px;
}
.beta__kicker i { margin-right: 8px; }
.beta__head h1 { margin: 0 0 14px; }
/* #f900b7 is the APP's magenta, the one .lockup__word span uses, not the
   stylesheet's older --brand: the title should match the product. */
.beta__mag { color: #f900b7; }
.beta__grn { color: var(--neon-green); }
.beta__lede { font-size: 17px; color: var(--text-dim); }

.beta__version {
  display: inline-block; margin-top: 12px;
  font-family: Comfortaa, system-ui, sans-serif; font-size: 13px;
  padding: 5px 12px; border-radius: 999px;
  border: 1px solid rgba(39, 248, 54, 0.35);
  background: rgba(39, 248, 54, 0.07);
}
.beta__version b { color: var(--neon-green); }

.beta__block {
  padding: 26px 0;
  border-top: 1px solid rgba(255, 255, 255, 0.09);
}
.beta__block h2 { margin: 0 0 12px; }
.beta__block h2 i { color: var(--neon-green); margin-right: 10px; }
.beta__block p { color: var(--text-dim); }
.beta__sub { margin: 22px 0 10px; font-size: 17px; }
.beta__note { font-size: 14px; color: var(--text-faint); margin-top: 14px; }
.beta__actions { margin-top: 18px; }

.beta__steps, .beta__list { color: var(--text-dim); padding-left: 22px; }
.beta__steps li, .beta__list li { margin-bottom: 12px; line-height: 1.55; }
.beta__steps b, .beta__list b { color: var(--text); }

/* The two routes to a report, side by side, so neither reads as the fallback
   for the other -- they are a preference, not a hierarchy. */
.beta__ways {
  display: grid; grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
  gap: 14px; margin: 10px 0 6px;
}

/* The bug board, which is THE answer rather than one of three: full width,
   brighter, and above the pair that are only for when it is not an option. */
.betaway--lead {
  margin: 18px 0 4px;
  border-color: rgba(39, 248, 54, 0.35);
  background:
    linear-gradient(120deg, rgba(39, 248, 54, 0.10), rgba(0, 0, 0, 0.36) 62%);
}
.betaway--lead > i { font-size: 26px; }
.betaway--lead .betaway__what { font-size: 20px; }

.beta__aside {
  margin: 22px 0 0; font-size: 13px; color: var(--text-faint);
}
.betaway {
  display: block; padding: 16px 18px;
  border: 1px solid rgba(255, 255, 255, 0.10); border-radius: var(--radius);
  background: rgba(0, 0, 0, 0.32);
  color: var(--text-dim); text-decoration: none;
  transition: transform var(--t) var(--ease), border-color var(--t) var(--ease);
}
.betaway:hover {
  transform: translateY(-2px); text-decoration: none;
  border-color: rgba(39, 248, 54, 0.45);
}
.betaway > i { font-size: 22px; color: var(--neon-green); }
.betaway__what {
  display: block; margin: 8px 0 4px; color: var(--text);
  font-family: Comfortaa, system-ui, sans-serif; font-size: 17px;
}
.betaway__how { display: block; font-size: 14px; line-height: 1.5; }

.beta__patreon { background: linear-gradient(120deg, rgba(255, 66, 77, 0.06), transparent 60%); }

/* The shelf. Three cases, and everything under each one is centred on it
   rather than on the column, so the row reads as three objects. */
.betapacks {
  display: flex; flex-wrap: wrap; gap: 22px; margin: 22px 0 4px;
}
.betapack {
  appearance: none; background: transparent; border: 0; padding: 0;
  cursor: pointer; font: inherit; width: 176px; text-align: center;
  color: var(--text-dim);
  transition: transform 140ms ease, color 140ms ease;
}
.betapack:hover { transform: translateY(-3px); color: var(--text); }
.betapack__name {
  display: block; margin-top: 10px; color: var(--text);
  font-family: Comfortaa, system-ui, sans-serif; font-size: 15px; line-height: 1.3;
}
.betapack__len { display: block; margin-top: 4px; font-size: 12px; color: var(--text-faint); }
.betapack__price {
  display: inline-block; margin-top: 8px; padding: 3px 12px;
  font-family: Comfortaa, system-ui, sans-serif; font-weight: 700; font-size: 15px;
  border: 1px solid rgba(255, 255, 255, 0.16); border-radius: 999px;
  color: var(--neon-green);
}

.beta__thanks {
  max-width: none;
  margin-top: 30px; padding-top: 22px;
  border-top: 1px solid rgba(255, 255, 255, 0.09);
  color: var(--text-dim); font-size: 15px;
}

/* ====================================================== admin: email templates

   A shelf of the messages the site sends, then the one being edited, then what
   it will look like. The preview is an iframe with its own document, so what is
   on screen is what the recipient gets rather than what this stylesheet would
   make of it. */

.admtemplates {
  display: grid; grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
  gap: 12px; margin-top: 6px;
}
.admtemplate {
  appearance: none; text-align: left; cursor: pointer; font: inherit;
  padding: 14px 16px;
  border: 1px solid rgba(255, 255, 255, 0.10);
  border-radius: var(--radius);
  background: rgba(0, 0, 0, 0.30);
  color: var(--text-dim);
  transition: border-color var(--t) var(--ease), background var(--t) var(--ease);
}
.admtemplate:hover { border-color: rgba(255, 255, 255, 0.22); }
.admtemplate.is-on {
  border-color: rgba(150, 200, 255, 0.55);
  background: rgba(150, 200, 255, 0.08);
}
.admtemplate__name {
  display: block; color: var(--text); font-size: 15px; margin-bottom: 4px;
}
.admtemplate__note { display: block; font-size: 12px; line-height: 1.5; }
.admtemplate__meta { display: block; margin-top: 8px; }

/* The placeholder vocabulary, inline in the sentence that explains it. */
.admph {
  display: inline-block; margin: 0 4px 4px 0; padding: 1px 6px;
  border: 1px solid rgba(255, 255, 255, 0.14);
  background: rgba(0, 0, 0, 0.4);
  font-family: ui-monospace, Consolas, monospace; font-size: 12px;
  color: var(--blue);
}

.admwarn {
  display: flex; align-items: flex-start; gap: 8px;
  margin: 0 0 14px; font-size: 13px; color: var(--amber);
}

/* Tall enough to hold a whole short message without scrolling, which is the
   point: a preview you have to scroll is a preview you do not read. */
.admpreview {
  display: block; width: 100%; height: 560px;
  border: 1px solid rgba(255, 255, 255, 0.10);
  background: #0f0f14;
}

/* ================================================== friends, and talking to them
   Three pieces: the button and panel in the menu bar, the presence dot that is
   used in both, and the chat windows along the bottom.

   THE DOT IS ITS OWN THING on purpose. It is drawn on an avatar in the panel, in
   a chat window's header and nowhere else yet -- but "where somebody is" is a
   fact about a person, and the moment it appears on a profile or a track page it
   should already look the same. */

.presence {
  position: absolute;
  right: -1px; bottom: -1px;
  width: 11px; height: 11px;
  border-radius: 50%;
  /* The ring is the panel's own background rather than a border colour, so the
     dot reads as sitting on top of the face instead of being part of it. */
  box-shadow: 0 0 0 2px #14151a;
}
.presence--on   { background: var(--green);      box-shadow: 0 0 0 2px #14151a, 0 0 8px rgba(96, 224, 160, 0.9); }
.presence--away { background: var(--amber);      box-shadow: 0 0 0 2px #14151a; }
.presence--off  { background: var(--text-faint); box-shadow: 0 0 0 2px #14151a; }

/* ----------------------------------------------------------- the menu item */

/* While its panel is open the button stays lit, in the same green the hover
   uses. Without it the bar says nothing about which menu you are looking at:
   the moment the pointer moves off to reach the panel, the button it came from
   goes dark. */
.bar .bar-item.button.friends__button.is-on {
  background-color: rgba(96, 224, 160, 0.13) !important;
  color: #eafff2 !important;
  box-shadow: inset 0 0 0 1px rgba(96, 224, 160, 0.30);
}
.bar .bar-item.button.friends__button.is-on i { color: var(--green); }

/* The padding is the badge's seat. Without it the count sits on top of the last
   letter of the word, and the alternative -- putting the badge after the label
   in the flow -- makes the whole bar shift sideways the moment somebody
   messages you. */
.friends__button { position: relative; padding-right: 28px !important; }

.friends__badge {
  position: absolute;
  top: 7px; right: 4px;
  min-width: 16px; height: 16px;
  padding: 0 4px;
  border-radius: 8px;
  background: var(--brand);
  color: #fff;
  font-size: 10px; font-weight: 600; line-height: 16px;
  text-align: center;
  box-shadow: 0 0 10px rgba(255, 0, 216, 0.6);
}

.friends__panel { width: 330px; max-width: calc(100vw - 24px); padding: 0; }
.friends__section { padding: 6px; }

.friends__heading {
  display: flex; align-items: baseline; gap: 8px;
  padding: 8px 8px 6px;
  color: var(--text-faint);
  font-size: 11px; letter-spacing: 0.08em; text-transform: uppercase;
}
.friends__count { margin-left: auto; letter-spacing: 0; text-transform: none; }

/* -- a request, which is the one thing here with two answers --------------- */

.friends__ask {
  display: grid;
  grid-template-columns: auto 1fr;
  gap: 4px 10px;
  padding: 8px;
  background: rgba(255, 0, 216, 0.06);
  border-left: 2px solid var(--brand);
}
.friends__askwho { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
.friends__askwho a { color: var(--text); font-size: 14px; text-decoration: none; }
.friends__askwho a:hover { color: var(--brand-soft); }
.friends__asksub { color: var(--text-faint); font-size: 12px; }
/* Under the name rather than beside it: two buttons and an avatar in one row
   leaves the name about forty pixels, and names here are long. */
.friends__askacts { grid-column: 2; display: flex; gap: 6px; margin-top: 4px; }

/* -- the list ------------------------------------------------------------- */

/* Capped at about six rows and scrolled past that. A popover that grows with
   the buddy list eventually runs off the bottom of the window, and Popover
   flips it upwards, which for a long list means it covers the page instead. */
.friends__list { max-height: 320px; overflow-y: auto; }

.friends__row { display: flex; align-items: center; }

.friends__who {
  display: flex; align-items: center; gap: 10px;
  flex: 1 1 auto; min-width: 0;
  padding: 7px 8px;
  border: 0; background: transparent; color: inherit; font: inherit;
  text-align: left; cursor: pointer;
}
.friends__who:hover { background: rgba(255, 255, 255, 0.06); }

.friends__face { position: relative; flex: 0 0 auto; line-height: 0; }
.friends__text { display: flex; flex-direction: column; gap: 1px; min-width: 0; }
.friends__name { color: var(--text); font-size: 14px; }

/* One line, ever. The sentence is composed by the other person's client and can
   be a track title long; wrapping it would make one buddy's row three rows. */
.friends__doing {
  color: var(--text-faint); font-size: 12px;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}

.friends__unread {
  flex: 0 0 auto;
  min-width: 18px; padding: 0 5px;
  border-radius: 9px;
  background: var(--brand);
  color: #fff; font-size: 11px; font-weight: 600; line-height: 18px;
  text-align: center;
}

/* Faint until the row is under the pointer: removing somebody is not what this
   list is for, and a row of little crosses reads like a list of things to
   close. */
.friends__drop {
  flex: 0 0 auto;
  padding: 6px 8px;
  border: 0; background: transparent;
  color: var(--text-faint); font-size: 12px;
  opacity: 0; cursor: pointer;
  transition: opacity var(--t) var(--ease);
}
.friends__row:hover .friends__drop { opacity: 1; }
.friends__drop:hover { color: #ff8080; }
.friends__drop.is-sure { opacity: 1; color: #ff8080; }

.friends__sent {
  display: flex; align-items: center; gap: 8px;
  padding: 5px 8px; font-size: 13px; color: var(--text-dim);
}
.friends__sent button { margin-left: auto; }

/* -- finding somebody ------------------------------------------------------ */

.friends__find { padding: 6px 6px 10px; }
.friends__find .input { width: 100%; font-size: 13px; padding: 7px 9px; }

.friends__found {
  display: flex; align-items: center; gap: 9px;
  padding: 6px 2px;
}
.friends__foundname {
  flex: 1 1 auto; min-width: 0;
  color: var(--text); font-size: 14px; text-decoration: none;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.friends__foundname:hover { color: var(--brand-soft); }
.friends__asked { color: var(--text-faint); font-size: 12px; }

/* -- and on a profile ------------------------------------------------------ */

.buddybtn { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; margin-top: 12px; }
.buddybtn__note { color: var(--text-dim); font-size: 13px; }
.buddybtn__note i { color: var(--amber); margin-right: 5px; }

/* ========================================================== the chat windows
   Along the bottom edge, and ABOVE THE PLAYER rather than over it: the bar is a
   control somebody is using while they talk, so the dock clears it. 96px is the
   bar's height plus the hairline above it; when nothing is playing the bar is
   not there and the gap is simply a margin. */

.chatdock {
  position: fixed;
  right: 16px; bottom: 96px;
  /* Nothing playing means no player bar to clear. Set from the queue rather
     than guessed in CSS, because the bar is absent from the document
     altogether when it is empty -- there is nothing here to measure against. */
  z-index: 8900;                 /* under the player, over the page */
  display: flex; align-items: flex-end; gap: 12px;
  pointer-events: none;          /* the gaps between windows are still page */
}
.chatdock.is-clear { bottom: 16px; }
.chatdock > * { pointer-events: auto; }

.chat {
  display: flex; flex-direction: column;
  /* Wide enough that a face and a bubble are not shoulder to shoulder. Three
     of these plus their gaps still sit inside a 1280 laptop, which is the
     constraint -- MAX_OPEN is 3. */
  width: 350px; max-height: 440px;
  border: 1px solid var(--border);
  background: var(--panel-a);
  -webkit-backdrop-filter: blur(22px) saturate(130%);
  backdrop-filter: blur(22px) saturate(130%);
  box-shadow: 0 18px 44px rgba(0, 0, 0, 0.6);
}
/* The lit hairline the player and the nav use, so a chat window reads as part
   of the same chrome rather than a box dropped on the page. */
.chat::before {
  content: "";
  height: 1px;
  background: var(--grad-brand);
  opacity: 0.75;
}
.chat.is-rolled { max-height: none; }

.chat__head { display: flex; align-items: center; background: var(--header-a); }

/* A link to their profile now rather than the roll-up control, so it wants a
   link's manners: no underline, and the same green everything else on this site
   lights up with. */
.chat__who {
  display: flex; align-items: center; gap: 9px;
  flex: 1 1 auto; min-width: 0;
  padding: 8px 10px;
  border: 0; background: transparent; color: inherit; font: inherit;
  text-align: left; cursor: pointer; text-decoration: none;
}
.chat__who:hover { background: rgba(96, 224, 160, 0.10); }
.chat__who:hover .chat__name { color: var(--green); }
.chat__face { position: relative; flex: 0 0 auto; line-height: 0; }
.chat__face .presence { box-shadow: 0 0 0 2px #101015; }
.chat__headtext { display: flex; flex-direction: column; gap: 1px; min-width: 0; }
.chat__name { color: var(--text); font-size: 14px; }
.chat__doing {
  color: var(--text-faint); font-size: 11px;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}

.chat__icon {
  flex: 0 0 auto;
  padding: 8px 10px;
  border: 0; background: transparent;
  color: var(--text-faint); font-size: 13px; cursor: pointer;
}
.chat__icon:hover:not(:disabled) { color: var(--text); }
.chat__icon:disabled { opacity: 0.4; cursor: default; }

/* Where they are, as somewhere to go. Only drawn when presence carried a url. */
.chat__jump {
  display: flex; align-items: center; gap: 7px;
  padding: 6px 10px;
  border-bottom: 1px solid rgba(255, 255, 255, 0.07);
  background: rgba(96, 224, 160, 0.07);
  color: var(--green); font-size: 12px; text-decoration: none;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.chat__jump:hover { background: rgba(96, 224, 160, 0.14); }

.chat__log {
  flex: 1 1 auto;
  overflow-y: auto;
  padding: 10px;
  display: flex; flex-direction: column; gap: 6px;
}
.chat__empty { margin: auto 0; color: var(--text-faint); font-size: 13px; text-align: center; }
.chat__earlier {
  align-self: center;
  margin-bottom: 4px; padding: 4px 10px;
  border: 1px solid var(--border); background: transparent;
  color: var(--text-dim); font-size: 12px; cursor: pointer;
}
.chat__earlier:hover { color: var(--text); border-color: rgba(255, 255, 255, 0.3); }

.chat__error { padding: 6px 10px; color: #ff8f8f; font-size: 12px; }

.chat__composer {
  display: flex; align-items: flex-end; gap: 2px;
  padding: 6px;
  border-top: 1px solid rgba(255, 255, 255, 0.08);
  background: rgba(0, 0, 0, 0.25);
}
.chat__draft {
  flex: 1 1 auto; min-width: 0;
  /* Grows with what is being typed and stops at four lines, past which it
     scrolls -- a box that keeps growing eventually eats the conversation it is
     part of. */
  max-height: 88px; min-height: 34px;
  padding: 8px;
  border: 1px solid transparent; border-radius: var(--radius);
  background: rgba(255, 255, 255, 0.05);
  color: var(--text); font: inherit; font-size: 13px;
  resize: none;
}
.chat__draft:focus { outline: 0; border-color: rgba(150, 200, 255, 0.45); }
.chat__send {
  flex: 0 0 auto;
  padding: 8px 10px;
  border: 0; background: transparent;
  color: var(--green); font-size: 14px; cursor: pointer;
}
.chat__send:disabled { color: var(--text-faint); opacity: 0.5; cursor: default; }

/* -- one message ----------------------------------------------------------
   A FACE, AND A BUBBLE POINTING AT IT. The tail is what makes this speech
   rather than a stack of boxes, and it only means anything if it points at
   somebody -- so the pointer and the picture arrive together, on the first
   message of a run, and later messages from the same person tuck in under it
   with the face's slot held open so they stay in line.

   Two triangles per tail, not one. These bubbles carry a 1px border, and a
   single triangle in the fill colour would leave the point looking cut off
   where the outline stops. The outer one is the border colour, the inner one
   sits a pixel inside it in the fill -- which is the standard way to carry an
   outline around a CSS triangle, and the reason each tail is two rules. */

.bubbleline { display: flex; flex-direction: column; align-items: stretch; }
/* A continuation sits closer to the line above than a new speaker does: the
   gap is what says "still them". */
.bubbleline.is-run { margin-top: -3px; }

.bubble__when {
  align-self: center;
  margin: 6px 0 4px;
  color: var(--text-faint); font-size: 11px;
}

.bubble { display: flex; align-items: flex-end; gap: 10px; max-width: 100%; }
/* Yours reads right to left: face on the right, bubble beside it, tail on the
   other side. One declaration, because everything below is written against the
   flex direction rather than against left and right. */
.bubbleline--mine .bubble { flex-direction: row-reverse; }

/* Held open whether or not it has a picture in it. */
.bubble__face { flex: 0 0 26px; width: 26px; line-height: 0; align-self: flex-end; }
.bubble__face a { display: block; border-radius: 50%; }
.bubble__face a:hover { box-shadow: 0 0 0 2px var(--green); }

/* OPAQUE, WHERE ALMOST EVERYTHING ELSE ON THIS SITE IS NOT, and that is what
   makes the tail possible. A triangle is drawn with a border colour, so a
   translucent fill would composite against the panel a second time and come out
   a different shade from the bubble it belongs to -- the join shows as a seam
   right at the point, which is the one place the eye is going. These two are
   the old rgba fills flattened onto the panel behind them, so nothing changed
   except that the colour is now stated once instead of computed twice. */
.bubble__body {
  position: relative;
  /* The face (26) plus its gap (10), and a margin so the longest line does not
     run right up to the far wall of the window. */
  max-width: calc(100% - 52px);
  padding: 7px 10px;
  border: 1px solid rgba(255, 255, 255, 0.08);
  background: #222227;
}
/* Yours is the lit one. The difference is colour rather than side alone, so a
   run of messages from one person is legible without counting the alignment. */
.bubbleline--mine .bubble__body {
  border-color: rgba(255, 0, 216, 0.30);
  background: #301431;
}

/* -- the tail -------------------------------------------------------------- */

.bubble__body.has-tail::before,
.bubble__body.has-tail::after {
  content: "";
  position: absolute;
  bottom: 6px;
  width: 0; height: 0;
  border: 7px solid transparent;
}

/* Theirs: the point is on the left, aimed at the face beside it. */
.bubble__body.has-tail::before {
  left: -8px;
  border-left: 0;
  border-right-color: rgba(255, 255, 255, 0.16);
}
.bubble__body.has-tail::after {
  left: -6px;
  border-left: 0;
  border-right-color: #222227;
}

/* Yours: the same thing, mirrored, in the brand tint. */
.bubbleline--mine .bubble__body.has-tail::before {
  left: auto; right: -8px;
  border-right: 0;
  border-left-color: rgba(255, 0, 216, 0.34);
}
.bubbleline--mine .bubble__body.has-tail::after {
  left: auto; right: -6px;
  border-right: 0;
  border-left-color: #301431;
}

.bubble__text {
  margin: 0;
  color: var(--text); font-size: 13px; line-height: 1.45;
  /* A pasted url has no spaces in it and will happily be six hundred pixels
     wide; anywhere is the only break rule that holds it inside the window. */
  overflow-wrap: anywhere;
  /* Newlines somebody typed are newlines. */
  white-space: pre-wrap;
}
.bubble__text a { color: var(--blue); }

.bubble__image { display: block; line-height: 0; }
.bubble__image img { max-width: 100%; max-height: 220px; display: block; }
.bubble__loading {
  display: block; width: 120px; height: 80px;
  background: rgba(255, 255, 255, 0.06);
}

.bubble__file {
  display: flex; align-items: center; gap: 8px;
  color: var(--text); font-size: 13px; text-decoration: none;
}
.bubble__file i { color: var(--blue); }
.bubble__filename { overflow-wrap: anywhere; }
.bubble__filesize { color: var(--text-faint); font-size: 11px; white-space: nowrap; }
.bubble__file--gone { color: var(--text-faint); font-style: italic; }
/* A download link with no blob behind it yet is not a link. */
.bubble__file[aria-disabled="true"] { opacity: 0.6; pointer-events: none; }

/* On a phone one window fills the width and the others wait behind the menu
   bar's badge, which is where they were opened from. */
@media (max-width: 700px) {
  .chatdock { left: 8px; right: 8px; bottom: 88px; }
  .chat { width: 100%; max-height: 60vh; }
  .chatdock > .chat:not(:last-child) { display: none; }
}

/* ---------------------------------------------------- software preservation --

   The page's own column, the same 1220 every other page holds. See the note in
   Preservation.tsx: .container is the full-bleed band, and the block inside it
   is what caps the content -- .hero and .feature do this for every other page,
   and this one had nothing doing it at all. */
.preserved-page { max-width: var(--page); margin-inline: auto; }


   The shelf of shipped games that now run in a browser, and the page in front
   of one of them. Both are ordinary pages of this site -- the game itself
   arrives in an iframe from its own container -- so they are dressed from the
   same tokens as everything above rather than from a stylesheet of their own.

   The box art is the whole visual: these are 3D case renders on the CDN, drawn
   for a store page and already carrying their own perspective and shadow. They
   are CAPPED rather than fluid, because at full column width one of them is
   900px tall and pushes everything a visitor actually needs -- the controls --
   below the fold. */

/* A fixed track, not a fluid one: DvdBox is built from pixel depths and takes a
   width rather than filling its column, so the track is the case's own width
   with room for the tilt it does under the cursor. */
.preserved-shelf {
  display: grid;
  grid-template-columns: repeat(auto-fill, 200px);
  gap: 34px 30px;
  margin: 32px 0 8px;
  justify-content: start;
}

.preserved-card {
  display: flex;
  flex-direction: column;
  gap: 12px;
  color: var(--text);
  text-decoration: none;
}

/* No lift on hover: the case does its own, following the cursor, and two things
   moving at once reads as a wobble. */
.preserved-card b { font-weight: 600; line-height: 1.25; }
.preserved-card b span { font-weight: 300; color: var(--text-dim); }
.preserved-card small { color: var(--text-faint); font-size: 12px; }

/* The note a title carries when there is something to know BEFORE pressing
   play -- ezmuze+ 2.0 needs a physical controller and cannot say so itself. */
.preserved-caveat {
  border-left: 3px solid var(--amber);
  padding: 0.1em 0 0.1em 0.9em;
  margin: 1.2em 0;
  color: var(--text-dim);
  max-width: 56ch;
}

.preserved-controls { width: 100%; max-width: 56ch; border-collapse: collapse; }
.preserved-controls th {
  text-align: left; padding: 0 0.7em 0.5em 0; color: var(--text-faint);
  font-weight: 500; letter-spacing: 0.07em; text-transform: uppercase; font-size: 11px;
}
.preserved-controls td { padding: 0.32em 0.7em 0.32em 0; border-top: 1px solid var(--border); color: var(--text-dim); }
.preserved-controls td:first-child { width: 52%; color: var(--text); }
.preserved-controls kbd {
  display: inline-block; padding: 0.15em 0.5em; border-radius: 4px;
  background: var(--raised); border: 1px solid var(--border); border-bottom-width: 2px;
  font: inherit; font-size: 0.85em; color: var(--text); white-space: nowrap;
}

/* The game, over the page. Fixed to the viewport so the site's own scrollbar,
   nav and footer are all behind it rather than around it. */
.preserved-stage { position: fixed; inset: 0; z-index: 4000; background: #000; }

/* Loading behind the page. Full size, so the game measures the window it will
   actually be shown at -- a shrunken frame would size its canvas to the wrong
   thing and a hidden one would stop the animation frames its boot runs on. */
.preserved-stage--loading {
  z-index: 0;
  opacity: 0;
  pointer-events: none;
}
.preserved-stage iframe { border: 0; width: 100%; height: 100%; display: block; }

/* PLAYING IN THE BOX ART'S PLACE. The rectangle comes from JS, measured off the
   slot every frame -- the frame is SIZED to it rather than moved into it,
   because reparenting an iframe reloads it and reloading is the game starting
   over. Still fixed, still in the portal, so nothing on the page can draw over
   it; only its inset changes. */
.preserved-stage--inline {
  inset: auto;
  border-radius: 6px;
  overflow: hidden;
  box-shadow: 0 18px 44px rgba(0, 0, 0, 0.55);
}

/* Filling the browser, which is where it used to go straight away. */
.preserved-stage--wide { inset: 0; border-radius: 0; }

/* The buttons live OUT HERE, in this document, over the frame -- they cannot be
   keys, because once the canvas has focus the keyboard belongs to the game, and
   Escape in particular is a key these titles use for their own menu. */
.preserved-stage__bar {
  position: absolute;
  top: 8px; right: 8px;
  z-index: 4001;
  display: flex;
  gap: 6px;
  opacity: 0.55;
  transition: opacity .16s ease;
}
.preserved-stage:hover .preserved-stage__bar { opacity: 1; }
.preserved-stage--wide .preserved-stage__bar { position: fixed; top: 12px; right: 12px; }
.preserved-stage__close { opacity: 0.85; }
.preserved-stage__close:hover { opacity: 1; }

/* THE SHAPE CHANGES WHEN THE GAME TAKES THE SLOT, and it has to: a game case is
   portrait and every one of these titles draws 16:9. Left at the case's shape the
   picture would sit letterboxed in a tall grey box, which is a worse frame for it
   than the box art it replaced. So the slot becomes the shape of the thing in it. */
/* THE WHOLE HERO, ONCE IT IS PLAYING. The blurb, the badges and the play button
   have all done their job by then -- they exist to get somebody INTO the game --
   and leaving them beside it wastes half the width on text nobody is reading any
   more. So the copy goes and the game takes the full column, centred, which is
   also the widest it can be without covering the page. */
.hero--playing { grid-template-columns: minmax(0, 1fr); }
.hero--playing .hero__copy { display: none; }
.hero--playing .hero__media { width: 100%; margin: 0 auto; }

.preserved-slot--playing {
  width: 100%;
  aspect-ratio: 16 / 9;
  min-height: 200px;

  /* CAPPED BY HEIGHT, NOT BY WIDTH. Sixteen by nine across a wide page is eight
     hundred pixels tall, which pushes the controls table -- the thing somebody
     reads next -- clean off the screen. Capping the WIDTH is what keeps the
     shape: cap the height instead and the box goes short and the picture
     letterboxes inside it. */
  max-width: min(100%, calc(70vh * 16 / 9));
  margin: 0 auto;
}

/* The standing notice on the preservation pages. Quiet: it is a footnote to
   the page, not a warning about the software, and it sits under the thing it
   qualifies rather than in front of the play button. */
.preserved-notice {
  max-width: 72ch;
  margin: 40px 0 8px;
  padding: 16px 20px;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--panel-a);
}

.preserved-notice h4 {
  margin: 0 0 0.5em;
  font-size: 0.8rem;
  font-weight: 600;
  letter-spacing: 0.07em;
  text-transform: uppercase;
  color: var(--text-faint);
}

.preserved-notice h4 i { color: var(--amber); margin-right: 0.5em; }
.preserved-notice p { margin: 0; font-size: 0.88rem; line-height: 1.6; color: var(--text-dim); }
.preserved-notice b { color: var(--text); font-weight: 600; }

/* ---------------------------------------------------------------- invite ---
   The buddy picker a preserved title opens to ask who a song should go to.
   It borrows the auth card, because it IS that card with a list in it, and a
   window opened from a game should not be a differently shaped thing from the
   window opened to sign that game in. */
.invite__list { list-style: none; margin: 1rem 0 0; padding: 0; }
.invite__list li { margin: 0 0 .4rem; }

.invite__buddy {
    display: flex; flex-direction: column; align-items: flex-start; gap: .15rem;
    width: 100%; padding: .6rem .8rem; text-align: left; cursor: pointer;
    border: 1px solid rgba(255, 255, 255, .14); border-radius: 6px;
    background: rgba(255, 255, 255, .04); color: inherit; font: inherit;
}
.invite__buddy:hover:not(:disabled) { background: rgba(255, 255, 255, .09); }
.invite__buddy:disabled { opacity: .5; cursor: default; }

.invite__name { font-weight: 600; }
.invite__where { font-size: .82em; opacity: .7; }

/* Somebody in THIS game can open the song the moment it arrives, which is worth
   saying in colour rather than leaving in the sentence. */
.invite__where--here { color: #7ddc85; opacity: 1; }

/* An invite in the chat: the way somebody who is not playing accepts one. */
.bubble__invite {
    display: inline-block; margin: .35rem 0 .1rem; padding: .35rem .7rem;
    border-radius: 5px; text-decoration: none; font-size: .85em; font-weight: 600;
    background: rgba(125, 220, 133, .16); color: #7ddc85;
    border: 1px solid rgba(125, 220, 133, .35);
}
.bubble__invite:hover { background: rgba(125, 220, 133, .26); }

/* A licence row in the sales table unfolds to show who bought it. */
.admrow--clickable { cursor: pointer; }
.admrow--clickable:hover { background: rgba(255, 255, 255, .05); }
.admcaret { width: .8em; opacity: .55; }

.admrow--unfold > td { padding: .4rem .6rem .8rem 2rem; background: rgba(0, 0, 0, .18); }
.admtable--inner { width: auto; min-width: 22rem; margin: 0; font-size: .92em; }
.admtable--inner th { font-weight: 600; opacity: .6; }

/* The last twenty purchases beside the last twenty top-ups.
   TWO COLUMNS, NOT ONE LIST: they are different events -- a purchase can move
   no money and a top-up buys nothing -- and interleaving them would invite the
   two to be read as one running total. auto-fit rather than a media query, so
   they stack the moment a column would be too narrow to read rather than at
   some width picked in advance. */
.admsplit {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(20rem, 1fr));
  gap: 18px 28px;
  align-items: start;
}
.admsplit h4 { margin: 0 0 .5rem; font-size: .95em; font-weight: 600; opacity: .6; }
/* Inner tables size to their content where one unfolds under a row; here they
   share a grid track and should fill the one they are given. */
.admsplit .admtable--inner { width: 100%; min-width: 0; }

/* ---------------------------------------------------------------- cash out */
/* The Sigma page's cash-out section (module-store design §7.7). It reads like
   the top-up section above it -- a note, a short form, a statement -- because
   it is the same money going the other way. The amount box carries the live
   payout figure INSIDE it, next to what was typed, so the two numbers are read
   as one fact: this much sigma, that much money. */

.cashout__form { display: grid; gap: 14px; max-width: 520px; margin-bottom: 20px; }
.cashout__amountbox {
  display: flex; align-items: center; gap: 10px;
  min-height: 46px; padding: 0 12px;
  background: rgba(255, 255, 255, 0.04);
  border: 1px solid rgba(255, 255, 255, 0.14);
  border-radius: var(--radius);
}
.cashout__amountbox:focus-within { border-color: var(--blue); }
.cashout__sigma { font-weight: 700; color: var(--text-dim); }
.cashout__amountbox input {
  flex: 0 1 120px; min-width: 0;
  font-size: 16px; min-height: 0;
  background: transparent; border: 0; padding: 8px 0; color: var(--text);
}
.cashout__amountbox input:focus { outline: none; box-shadow: none; }
.cashout__payout { flex: 1 1 auto; font-size: 14px; color: var(--text-faint); white-space: nowrap; }
.cashout__payout b { color: var(--neon-green); font-variant-numeric: tabular-nums; }
.cashout__email input { font-size: 16px; min-height: 46px; }
.cashout__error { margin: 0; }
.cashout__done { margin: 0; font-size: 14px; color: var(--neon-green); }
.cashout__actions { display: flex; justify-content: flex-end; }

.cashout__subhead {
  margin: 0 0 8px; font-size: 12px; letter-spacing: 0.08em; text-transform: uppercase;
  color: var(--text-faint);
}
.cashout__table { width: 100%; border-collapse: collapse; font-size: 14px; }
.cashout__table th {
  text-align: left; font-weight: 600; font-size: 12px; color: var(--text-faint);
  padding: 6px 10px; border-bottom: 1px solid rgba(255, 255, 255, 0.1);
}
.cashout__table td { padding: 9px 10px; border-bottom: 1px solid rgba(255, 255, 255, 0.06); vertical-align: middle; }
.cashout__table tr:last-child td { border-bottom: 0; }
.cashout__num { font-variant-numeric: tabular-nums; white-space: nowrap; }
.cashout__note { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
.cashout__dim { color: var(--text-faint); font-size: 13px; }

/* The status as a word with a colour: paid is the only good outcome, rejected
   is the only bad one, and waiting is neither yet. Same three colours the
   admin's chips use, so a person and the person paying them see the same
   thing. */
.cashout__status {
  display: inline-block; font-size: 12px; line-height: 1;
  padding: 4px 9px; border-radius: 999px; white-space: nowrap;
  border: 1px solid rgba(255, 255, 255, 0.14); color: var(--text-dim);
}
.cashout__status.is-requested { border-color: rgba(255, 196, 64, 0.4); background: rgba(255, 196, 64, 0.1); color: var(--amber); }
.cashout__status.is-paid { border-color: rgba(80, 220, 140, 0.45); background: rgba(80, 220, 140, 0.10); color: #b6f5cf; }
.cashout__status.is-rejected { border-color: rgba(255, 120, 120, 0.4); background: rgba(255, 70, 70, 0.10); color: #ffb3b3; }

/* The admin's queue. The terms it is running under sit in the card's note, so
   "why is this person allowed to ask" is answered before the first row. A row
   unfolds for pay / reject rather than opening a dialog: the batch id is pasted
   from PayPal, and the request it belongs to should stay on screen while it
   is. */
.admcashout__terms { display: flex; gap: 14px; flex-wrap: wrap; align-items: center; }
.admcashout__terms b { color: var(--text); }
.admcashout__act { display: flex; gap: 8px; flex-wrap: wrap; align-items: center; }
.admcashout__act .adminput { max-width: 260px; }
.admcashout__mail { font-size: 12px; color: var(--text-faint); margin: 8px 0 0; }


/* ================================================ studio modules in the store

   The modules tab (#181) and the module half of a product page (#182). A
   module is a product like any other -- same case, same reviews, same buy
   button -- so what is here is only what a module has and a loop pack does
   not: a kind, a preview on the tile, a set of permissions, a version history
   and a lineage. Everything else reuses .pcard, .pdp and .rstars.
   -------------------------------------------------------------------------- */

/* -- the filter chips ----------------------------------------------------- */

/* The filter bar (FilterBar.tsx): groups of chips, in the store's column, one
   line down from the toolbar so head and shelf read as one block. */
.fbar {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 6px 18px;
  max-width: var(--page);
  margin: 0 auto 14px;
  padding-inline: clamp(16px, 5vw, 64px);
}

/* A group holds together on one line where it can: chips that wrap into the
   middle of the next group's chips lose the thing the label was for. */
.fbar__group { display: flex; flex-wrap: wrap; align-items: center; gap: 6px; }
.fbar__group--end { margin-left: auto; }

.fbar__label {
  font-size: 11px;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--text-faint);
  margin-right: 2px;
}

.fbar__count { font-size: 12px; color: var(--text-faint); }

.fchip {
  appearance: none;
  font: inherit;
  font-size: 13px;
  cursor: pointer;
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 5px 12px;
  border-radius: 999px;
  border: 1px solid rgba(255, 255, 255, 0.14);
  background: rgba(0, 0, 0, 0.3);
  color: var(--text-dim);
  transition: border-color var(--t) var(--ease), color var(--t) var(--ease),
              background var(--t) var(--ease);
}
.fchip:hover:not(:disabled) { color: var(--text); border-color: rgba(255, 255, 255, 0.3); }
.fchip.is-on {
  color: #fff;
  border-color: rgba(255, 5, 199, 0.55);
  background: rgba(255, 5, 199, 0.16);
  box-shadow: 0 0 18px -8px rgba(255, 5, 199, 0.9);
}
/* Nothing to find down there right now -- said quietly, because the chip comes
   back the moment another filter is let go. */
.fchip:disabled { opacity: 0.35; cursor: default; }

.fchip__n {
  font-size: 11px;
  font-variant-numeric: tabular-nums;
  color: var(--text-faint);
}
.fchip.is-on .fchip__n { color: rgba(255, 255, 255, 0.75); }

.fchip--clear { border-style: dashed; }

/* -- the shelf ------------------------------------------------------------ */

.mgrid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(190px, 1fr));
  gap: clamp(12px, 1.6vw, 22px);
  max-width: var(--page);
  margin: 0 auto clamp(24px, 4vw, 56px);
  padding-inline: clamp(16px, 5vw, 64px);
}

.mshelf__empty {
  max-width: var(--page);
  margin: 0 auto clamp(24px, 4vw, 56px);
  padding-inline: clamp(16px, 5vw, 64px);
  color: var(--text-faint);
  font-size: 14px;
}

/* A TALL tile, unlike the wide .pcard the other categories use: a module is
   picked on its sound and its author, and the preview button belongs at the
   top of that decision rather than at the end of a row. */
.mtile {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 2px;
  padding: 14px;
  border: 1px solid rgba(255, 255, 255, 0.09);
  border-radius: var(--radius);
  background: linear-gradient(150deg, rgba(58, 22, 74, 0.5), rgba(0, 0, 0, 0.4) 55%);
  color: var(--text);
  cursor: pointer;
  transition: transform var(--t) var(--ease), box-shadow var(--t) var(--ease),
              border-color var(--t) var(--ease);
}
.mtile:hover {
  transform: translateY(-2px);
  border-color: rgba(255, 5, 199, 0.45);
  box-shadow: 0 16px 34px -14px rgba(0, 0, 0, 0.8), 0 0 30px -14px rgba(255, 5, 199, 0.8);
  text-decoration: none;
}

.mtile__art { position: relative; align-self: center; margin-bottom: 10px; }

/* THE PREVIEW BUTTON, in the footer of whatever it is previewing.
   It used to be a 34px disc hung over the bottom-right corner of a module's
   case: the only tile on the store with anything drawn on top of its art, so a
   shelf of modules read as a different KIND of product from every other shelf,
   and the one piece of artwork the author made was the thing it covered. Small,
   in the corner of the footer, same place on every card that has one. */
.tileplay {
  width: 22px;
  height: 22px;
  flex: 0 0 auto;
  border-radius: 50%;
  border: 1px solid rgba(255, 255, 255, 0.22);
  background: rgba(255, 255, 255, 0.06);
  color: var(--text-dim);
  cursor: pointer;
  font-size: 9px;
  line-height: 1;
  padding: 0;
  display: grid;
  place-items: center;
  transition: transform var(--t) var(--ease), border-color var(--t) var(--ease),
              color var(--t) var(--ease);
}
/* The glyph is a triangle with no optical centre of its own, so it needs
   nudging right to look centred in a circle. Stop is square and does not. */
.tileplay .fa-play { margin-left: 1px; }
.tileplay:hover { border-color: var(--neon-green); color: var(--neon-green); }
.tileplay.is-on {
  border-color: var(--neon-green);
  color: var(--neon-green);
  box-shadow: 0 0 14px -6px rgba(39, 248, 54, 0.9);
}

.mtile__name {
  font-family: Comfortaa, system-ui, sans-serif;
  font-size: 16px;
  margin: 0;
  line-height: 1.25;
}
.mtile__by { font-size: 12px; color: var(--text-faint); }
.mtile__remix { font-size: 12px; color: var(--neon-violet); }

/* A fact, not a warning: the module is fine, this reader's studio is behind.
   Amber is what the rest of the site uses for "not yet", not for "wrong". */
.mtile__needs {
  margin-top: 6px;
  font-size: 11px;
  padding: 3px 8px;
  border-radius: 999px;
  border: 1px solid rgba(255, 196, 64, 0.4);
  background: rgba(255, 196, 64, 0.1);
  color: var(--amber);
}

.mtile__foot {
  margin-top: 8px;
  display: flex;
  align-items: center;
  gap: 8px;
  flex-wrap: wrap;
  width: 100%;
}
/* Whatever else is in the row, the preview sits at the far end of it. */
.mtile__foot .tileplay { margin-left: auto; }
.mtile__owned {
  font-size: 11px;
  padding: 3px 9px;
  border-radius: 999px;
  border: 1px solid rgba(39, 248, 54, 0.45);
  color: var(--neon-green);
}
.mtile__price {
  font-family: Comfortaa, system-ui, sans-serif;
  font-weight: 700;
  font-size: 14px;
  white-space: nowrap;
}
.mtile__price.is-free { color: var(--neon-green); }

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

.modmeta { color: var(--text-faint); }
.modmeta a { cursor: pointer; }

.modfacts {
  max-width: var(--page);
  margin: clamp(24px, 3.5vw, 40px) auto 0;
  padding-inline: clamp(16px, 5vw, 64px);
}

.modfacts__cols {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
  gap: clamp(20px, 3vw, 44px);
  align-items: start;
}
@media (max-width: 860px) { .modfacts__cols { grid-template-columns: minmax(0, 1fr); } }

.modfacts h3 {
  font-size: 12px;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--text-faint);
  margin: 0 0 10px;
}

.modfacts__block { margin-bottom: 26px; }
.modfacts__list { list-style: none; margin: 0; padding: 0; font-size: 14px; }
.modfacts__list li { margin-bottom: 6px; line-height: 1.5; }
.modfacts__muted { color: var(--text-faint); font-size: 13px; }

.modfacts__tier {
  margin: 12px 0 0;
  font-size: 13px;
  color: var(--text-faint);
  max-width: 62ch;
  line-height: 1.5;
}
.modfacts__tier b { color: var(--text-dim); font-weight: 600; }

/* -- what you can do ------------------------------------------------------ */

.permlist {
  list-style: none;
  margin: 0;
  padding: 0;
  border: 1px solid rgba(255, 255, 255, 0.09);
  border-radius: var(--radius);
  overflow: hidden;
}

/* The qualifier UNDER the sentence, not beside it. The mockup put it in a
   right-hand column, which works for "5% royalty" and falls apart on the real
   wording -- "remixes may be sold; 10% of each remix sale goes to the author"
   is longer than the line it qualifies, and squeezing the two into one row
   wrapped the sentence into three. Under it, indented to the text, the line
   reads as one thought either way. */
.permline {
  display: grid;
  grid-template-columns: 20px minmax(0, 1fr);
  align-items: baseline;
  gap: 4px 10px;
  padding: 10px 14px;
  font-size: 14px;
  border-bottom: 1px solid rgba(255, 255, 255, 0.06);
  background: rgba(0, 0, 0, 0.24);
}
.permline:last-child { border-bottom: 0; }
.permline i { font-size: 12px; }
.permline.is-yes i { color: var(--neon-green); }
.permline.is-no i { color: #ff7a7a; }
.permline.is-no .permline__text { color: var(--text-dim); }
.permline__text { line-height: 1.4; }
.permline__note { grid-column: 2; font-size: 12px; color: var(--text-faint); line-height: 1.4; }

/* -- the versions --------------------------------------------------------- */

.vlist { list-style: none; margin: 0; padding: 0; }

/* A left edge that says what taking this version costs a song already on the
   one before: green applies itself, amber has to be asked for. */
.vitem {
  padding: 10px 14px;
  margin-bottom: 8px;
  border-left: 3px solid rgba(39, 248, 54, 0.5);
  background: rgba(0, 0, 0, 0.24);
  border-radius: 0 var(--radius) var(--radius) 0;
}
.vitem.is-breaking { border-left-color: var(--amber); }

.vitem__head { display: flex; align-items: center; gap: 9px; flex-wrap: wrap; }
.vitem__head b { font-family: Comfortaa, system-ui, sans-serif; font-size: 15px; }
.vitem__date { font-size: 12px; color: var(--text-faint); }
.vitem__log { margin: 5px 0 0; font-size: 13px; color: var(--text-dim); line-height: 1.5; }
.vitem__diff { margin: 4px 0 0; font-size: 12px; color: var(--text-faint); }

.vtag {
  font-size: 11px;
  line-height: 1;
  padding: 3px 8px;
  border-radius: 999px;
  border: 1px solid rgba(255, 255, 255, 0.14);
  color: var(--text-dim);
}
.vtag--ok { border-color: rgba(39, 248, 54, 0.45); color: var(--neon-green); }
.vtag--break { border-color: rgba(255, 196, 64, 0.45); background: rgba(255, 196, 64, 0.1); color: var(--amber); }
.vtag--yanked { border-color: rgba(255, 120, 120, 0.45); background: rgba(255, 70, 70, 0.1); color: #ffb3b3; }

/* -- reporting one -------------------------------------------------------- */

.modreport { margin-top: 8px; display: flex; flex-direction: column; gap: 8px; align-items: flex-start; }
.modreport label { font-size: 13px; color: var(--text-dim); }
.modreport textarea {
  width: 100%;
  max-width: 520px;
  font: inherit;
  font-size: 13px;
  padding: 9px 11px;
  border-radius: var(--radius);
  border: 1px solid rgba(255, 255, 255, 0.14);
  background: rgba(0, 0, 0, 0.35);
  color: var(--text);
  resize: vertical;
}
.modreport__act { display: flex; gap: 8px; }
.modreport__said { margin: 0; font-size: 13px; color: #ff9a9a; }
.modreport__done { margin-top: 8px; }

/* =============================================================================
   The creator's side: my modules, my preset packs, and a preset pack's page
   (design §8, §7.9; tickets #183 and #192)
   =============================================================================

   ONE TABLE LANGUAGE. The dashboard, a version list and an earnings breakdown
   are the same table at three sizes, so they share `.crtable` rather than each
   inventing a grid: the row that opens is the outer one, and everything inside
   the panel is `.crtable--inner`, quieter and indented, so a reader can tell
   at a glance which level they are on.

   The colour vocabulary is the one the rest of the site already uses for the
   same ideas -- amber for "look at this", green for "fine", red for "broken" --
   so a yanked version and a rejected cash-out do not need to be learned twice.
   ========================================================================== */

.crhead { display: flex; align-items: baseline; justify-content: space-between; gap: 12px; flex-wrap: wrap; }
.crhead h3 { margin: 0; }

.crtable { width: 100%; border-collapse: collapse; font-size: 14px; margin-top: 12px; }
.crtable th {
  text-align: left; font-weight: 600; font-size: 12px; color: var(--text-faint);
  letter-spacing: 0.06em; text-transform: uppercase;
  padding: 6px 10px; border-bottom: 1px solid rgba(255, 255, 255, 0.1);
}
.crtable td { padding: 10px; border-bottom: 1px solid rgba(255, 255, 255, 0.06); vertical-align: middle; }
.crtable tr:last-child td { border-bottom: 0; }
.crnum { font-variant-numeric: tabular-nums; white-space: nowrap; }
.crdim { color: var(--text-faint); }
.crok { color: var(--green); }
.crroyalty { color: var(--text-faint); font-size: 12px; }

.crtable--inner { font-size: 13px; margin-top: 8px; background: rgba(0, 0, 0, 0.2); }
.crtable--inner th { padding: 5px 8px; }
.crtable--inner td { padding: 7px 8px; }
.crtable--inner tr.is-yanked td { opacity: 0.55; }

.crrow.is-open > td { background: rgba(255, 255, 255, 0.03); }
.crrow__name {
  background: none; border: 0; padding: 0; color: var(--text);
  font-size: 15px; font-weight: 600; cursor: pointer;
  display: inline-flex; align-items: center; gap: 8px;
}
.crrow__name:hover { color: var(--blue); }
.crrow__name i { font-size: 11px; color: var(--text-faint); width: 10px; }
.crrow__removed { font-size: 12px; color: #ffb3b3; margin-top: 4px; }

.cract { text-align: right; white-space: nowrap; }
.cract .button { font-size: 13px; padding: 5px 10px; margin-left: 6px; }

/* The status as a word with a colour. Draft is neutral because a draft is not
   a problem -- it is the normal state of something that has just been made. */
.crstatus {
  display: inline-block; font-size: 12px; line-height: 1;
  padding: 4px 9px; border-radius: 999px; white-space: nowrap;
  border: 1px solid rgba(255, 255, 255, 0.14); color: var(--text-dim);
}
.crstatus.is-published { border-color: rgba(80, 220, 140, 0.45); background: rgba(80, 220, 140, 0.1); color: #b6f5cf; }
.crstatus.is-unlisted { border-color: rgba(255, 196, 64, 0.4); background: rgba(255, 196, 64, 0.1); color: var(--amber); }
.crstatus.is-removed { border-color: rgba(255, 120, 120, 0.4); background: rgba(255, 70, 70, 0.1); color: #ffb3b3; }

.crchip {
  display: inline-block; margin-left: 6px; font-size: 11px; line-height: 1;
  padding: 3px 6px; border: 1px solid rgba(255, 255, 255, 0.16); color: var(--text-dim);
}
.crchip.is-bad { border-color: rgba(255, 120, 120, 0.4); color: #ffb3b3; }
.crchip.is-warn { border-color: rgba(255, 196, 64, 0.4); color: var(--amber); }

/* "breaking" is the one word on this page that costs somebody else something:
   a song on the old version does not move to it. Amber, everywhere it appears,
   so the table and the dry-run verdict say it the same way. */
.crbreaking {
  display: inline-block; margin-left: 6px; font-size: 11px; line-height: 1;
  padding: 3px 6px; background: rgba(255, 196, 64, 0.12);
  border: 1px solid rgba(255, 196, 64, 0.4); color: var(--amber);
}

.crpanelrow > td { padding: 0 !important; border-bottom: 1px solid rgba(255, 255, 255, 0.12) !important; }
.crpanel {
  padding: 16px; background: rgba(0, 0, 0, 0.28);
  border-left: 2px solid var(--brand);
}
.crtabs { display: flex; align-items: center; gap: 4px; margin-bottom: 14px; flex-wrap: wrap; }
.crtab {
  background: none; border: 1px solid transparent; color: var(--text-faint);
  padding: 5px 10px; font-size: 13px; cursor: pointer;
}
.crtab:hover { color: var(--text); }
.crtab.is-on { color: var(--text); border-color: rgba(255, 255, 255, 0.18); background: rgba(255, 255, 255, 0.05); }
.crpanel__flc { margin-left: auto; font-size: 12px; color: var(--text-faint); font-variant-numeric: tabular-nums; }
.crpanel__reports { font-size: 12px; color: var(--amber); margin-left: 12px; }

.crnote { font-size: 13px; color: var(--text-dim); margin: 0; }
.crwarn {
  font-size: 13px; color: var(--amber); margin: 0 0 12px;
  padding: 8px 10px; background: rgba(255, 196, 64, 0.08);
  border: 1px solid rgba(255, 196, 64, 0.28);
}

.crform { display: flex; flex-wrap: wrap; gap: 12px; }
.crfield { display: flex; flex-direction: column; gap: 5px; flex: 1 1 240px; min-width: 0; }
.crfield--wide { flex: 1 1 100%; }
.crfield > span { font-size: 12px; color: var(--text-faint); text-transform: uppercase; letter-spacing: 0.06em; }
.crfield input, .crfield textarea, .crfield select {
  background: rgba(0, 0, 0, 0.35); border: 1px solid rgba(255, 255, 255, 0.14);
  color: var(--text); padding: 8px 10px; font-size: 14px; border-radius: var(--radius);
  font-family: inherit; width: 100%;
}
.crfield input:focus, .crfield textarea:focus { outline: none; border-color: var(--blue); }
.crart { display: flex; align-items: center; gap: 12px; }
.crart img { width: 64px; height: 64px; object-fit: cover; border: 1px solid var(--border); }
.crpreview { width: 100%; max-width: 320px; }

.cractions { display: flex; align-items: center; justify-content: flex-end; gap: 10px; margin-top: 14px; flex-wrap: wrap; }
.cractions .crnote { margin-right: auto; }

.crnew {
  padding: 16px; margin-top: 12px;
  background: rgba(0, 0, 0, 0.28); border: 1px solid var(--border);
}
.crnew h4, .crnewversion h4 { margin: 0 0 8px; font-size: 15px; }
.crnewversion { margin-bottom: 18px; }

/* The dry run's answer. The border says which of the two it was before a word
   is read, which is the whole reason for asking before publishing. */
.crverdict {
  margin-top: 12px; padding: 12px;
  background: rgba(0, 0, 0, 0.3); border: 1px solid var(--border); border-left-width: 3px;
}
.crverdict.is-compatible { border-left-color: var(--green); }
.crverdict.is-breaking { border-left-color: var(--amber); }
.crverdict__head { display: flex; align-items: baseline; gap: 10px; flex-wrap: wrap; font-size: 14px; }
.crverdict__badge {
  font-size: 11px; text-transform: uppercase; letter-spacing: 0.06em;
  padding: 3px 7px; border: 1px solid rgba(255, 255, 255, 0.2); color: var(--text-dim);
}
.crverdict.is-compatible .crverdict__badge { border-color: rgba(80, 220, 140, 0.45); color: #b6f5cf; }
.crverdict.is-breaking .crverdict__badge { border-color: rgba(255, 196, 64, 0.45); color: var(--amber); }
.crdiff { margin: 10px 0 0 18px; font-size: 13px; color: var(--text-dim); }
.crdiff__inline { font-size: 12px; margin-top: 3px; }

.crchanges {
  margin-top: 12px; padding: 10px 12px; font-size: 13px;
  background: rgba(150, 200, 255, 0.06); border: 1px solid rgba(150, 200, 255, 0.22);
}
.crchanges p { margin: 0 0 6px; }
.crchanges p:last-child { margin-bottom: 0; }

.crearnings__totals { display: flex; gap: 24px; flex-wrap: wrap; margin-bottom: 16px; }
.crearnings__totals div { display: flex; flex-direction: column; gap: 3px; }
.crearnings__totals span { font-size: 12px; color: var(--text-faint); text-transform: uppercase; letter-spacing: 0.06em; }
.crearnings__totals strong { font-size: 20px; font-variant-numeric: tabular-nums; }
.crearnings h4 { margin: 0; font-size: 14px; }

.modulekey p { max-width: var(--measure); }

/* -- one file, dropped or chosen ------------------------------------------ */
.filedrop { margin: 8px 0; }
.filedrop__hit {
  display: flex; align-items: center; justify-content: center; gap: 10px;
  width: 100%; min-height: 64px; padding: 12px;
  background: rgba(0, 0, 0, 0.25); color: var(--text-dim);
  border: 1px dashed rgba(255, 255, 255, 0.22); border-radius: var(--radius);
  font-size: 14px; font-family: inherit; cursor: pointer;
  transition: border-color var(--t) var(--ease), color var(--t) var(--ease);
}
.filedrop__hit:hover { border-color: var(--blue); color: var(--text); }
.filedrop.is-over .filedrop__hit { border-color: var(--brand); color: var(--text); background: rgba(255, 0, 216, 0.06); }
.filedrop.is-off .filedrop__hit { opacity: 0.5; cursor: default; }

/* -- the permissions editor ----------------------------------------------- */
.permeditor { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); gap: 24px; margin-top: 12px; }
@media (max-width: 900px) { .permeditor { grid-template-columns: 1fr; } }

.permrow {
  display: flex; align-items: center; justify-content: space-between; gap: 12px;
  padding: 9px 0; border-bottom: 1px solid rgba(255, 255, 255, 0.06);
}
.permrow.is-off { opacity: 0.45; }
.permrow__label { display: flex; flex-direction: column; gap: 2px; font-size: 14px; }
.permrow__note { font-size: 12px; color: var(--text-faint); }

/* The one place on this site with a rounded control, and deliberately so:
   design-guide.md §4 keeps hard edges everywhere except knobs and switches. */
.permswitch {
  flex: none; width: 42px; height: 22px; padding: 0; cursor: pointer;
  background: var(--neutral); border: 1px solid var(--border); border-radius: 999px;
  transition: background var(--t) var(--ease);
}
.permswitch.is-on { background: var(--blue); border-color: var(--blue); }
.permswitch__knob {
  display: block; width: 16px; height: 16px; border-radius: 999px; background: #fff;
  transform: translateX(2px); transition: transform var(--t) var(--ease);
}
.permswitch.is-on .permswitch__knob { transform: translateX(22px); }

.permroyalty { display: flex; align-items: center; gap: 10px; padding: 9px 0; border-bottom: 1px solid rgba(255, 255, 255, 0.06); }
.permroyalty.is-off { opacity: 0.45; }
.permroyalty__label { flex: 1 1 auto; font-size: 13px; color: var(--text-dim); }
.permroyalty input[type=range] { flex: 0 1 140px; accent-color: var(--brand); }
.permroyalty__value { flex: none; width: 40px; text-align: right; font-variant-numeric: tabular-nums; font-size: 13px; }

.permtier { display: flex; align-items: center; justify-content: space-between; gap: 12px; padding: 12px 0; }
.permtier select {
  background: rgba(0, 0, 0, 0.35); border: 1px solid rgba(255, 255, 255, 0.14);
  color: var(--text); padding: 7px 10px; font-size: 14px; font-family: inherit;
}
.permtier__text { font-size: 12px; color: var(--text-faint); margin: 0 0 12px; max-width: var(--measure); }

.permeditor__rule { font-size: 13px; color: var(--text-dim); margin: 12px 0 0; max-width: var(--measure); }
.permeditor__warn {
  font-size: 13px; color: var(--amber); margin: 12px 0 0; padding: 8px 10px;
  background: rgba(255, 196, 64, 0.08); border: 1px solid rgba(255, 196, 64, 0.28);
}

.permeditor__matrix h4 { margin: 0 0 8px; font-size: 13px; text-transform: uppercase; letter-spacing: 0.06em; color: var(--text-faint); }
.permmatrix { list-style: none; margin: 0; padding: 0; }
.permmatrix li { display: flex; gap: 10px; align-items: baseline; padding: 7px 0; font-size: 14px; border-bottom: 1px solid rgba(255, 255, 255, 0.06); }
.permmatrix li i { flex: none; width: 14px; }
.permmatrix li.is-free i { color: var(--green); }
.permmatrix li.is-limited i { color: #ff8a8a; }
.permmatrix li small { color: var(--text-faint); }

/* -- a preset pack's page ------------------------------------------------- */
/* Two sentences that are not decoration: what you must ALSO have for these
   presets to be usable, and whether the module has changed shape since they
   were made. Amber for both, because both are things to know before buying
   rather than reasons not to. */
/* NOT a flex row. Its content is one SENTENCE with a link in the middle of it,
   and a flex gap between every text node and that link puts a space in front
   of the full stop ("get it here ."). */
.packneeds {
  margin: 12px 0; padding: 9px 12px; font-size: 14px; line-height: 1.5;
  background: rgba(255, 196, 64, 0.1); border: 1px solid rgba(255, 196, 64, 0.35);
  color: var(--amber);
}
.packneeds > i:first-child { margin-right: 8px; }
.packneeds a { color: inherit; text-decoration: underline; }
.packneeds.is-met {
  background: rgba(80, 220, 140, 0.1); border-color: rgba(80, 220, 140, 0.35); color: #b6f5cf;
}
.packstale {
  margin: 12px 0; padding: 9px 12px; font-size: 13px; color: var(--text-dim);
  background: rgba(0, 0, 0, 0.3); border: 1px solid var(--border);
}

/* The preset-pack case, in the brand's magenta: the four other product types
   have their own hue and a fifth without one would be a loop pack by default. */
.dvd--presets { --ph: 320; }
/* Content packs: amber, the one warm hue left. They are the only product in
   here that is somebody else's recordings rather than somebody's code, and on
   a shelf of green and purple cases that reads as a different kind of thing --
   which it is. */
.dvd--content { --ph: 38; }

/* A content pack's licence, on its tile. Quiet and last: it is a fact a
   buyer of a sample library checks, not the headline -- but it is the fact
   that decides whether they can use it, so it is on the shelf rather than
   one click away. */
.pcard__licence {
  margin-top: 6px;
  font-size: 11px;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--text-faint, #8a8a9a);
}

/* A preserved title's own preferences, on the page you launch it from.
   Folded away by default: it is a thing to reach for, not a thing to read past
   on the way to the play button. */
.preserved-settings { margin: 1.2em 0 0; }
.preserved-settings > summary {
    cursor: pointer; font-weight: 600; padding: .35em 0;
    color: var(--ink-dim, #a6a6b6);
}
/* The cog is the label's mark, not a second thing beside it: it takes the
   summary's colour on hover with the words rather than staying dim next to
   brightened text. */
.preserved-settings > summary .fa-cog { margin-right: .45em; opacity: .8; }
.preserved-settings > summary:hover .fa-cog { opacity: 1; }
.preserved-settings > summary:hover { color: var(--ink, #e9e9f0); }
.preserved-settings__note { font-size: .82em; opacity: .75; margin: .2em 0 .8em; }
.preserved-settings__grid {
    display: grid; grid-template-columns: repeat(auto-fit, minmax(230px, 1fr));
    gap: .35em 1.4em; margin-bottom: 1em;
}
.preserved-settings__row {
    display: flex; align-items: center; gap: .55em;
    font-size: .88em; cursor: pointer; padding: .15em 0;
}
.preserved-settings__row input[type="range"] { flex: 1; min-width: 90px; }
.preserved-settings__value { opacity: .7; min-width: 3.2em; text-align: right; }
.preserved-settings__said { font-size: .82em; opacity: .8; margin-left: .6em; }

/* What is running, on the admin dashboard. A table rather than cards: these rows
   exist to be compared with each other, and comparison wants columns. */
.deploys { width: 100%; border-collapse: collapse; font-size: .88em; }
.deploys th, .deploys td { text-align: left; padding: .3em .6em .3em 0; }
.deploys thead th { font-weight: 600; opacity: .6; font-size: .9em; }
.deploys code { font-size: .95em; opacity: .95; }
.deploys__group th { padding-top: .9em; font-weight: 600; opacity: .75; }
.deploys__ok { color: var(--positive, #60e0a0); font-weight: 600; }
.deploys__off { color: var(--amber, #e0a860); font-weight: 600; }
.deploys__down td { opacity: .85; }
.deploys__aside { opacity: .5; margin-left: .5em; font-size: .9em; }
