/* ==========================================================================
   ONE DIGITAL MUSIC — Design System
   --------------------------------------------------------------------------
   One theme, no toggle. Everything sits on graphite: the page is the
   darkest plane, panels step up, and inset boxes (wallet, stats) step up
   again so figures read as recessed panels. Text is white throughout.

   Flat throughout — not a single gradient. Depth comes from three greys,
   hairline borders and restrained shadows. Colour is reserved for status.
   ========================================================================== */

/* --------------------------------------------------------------------------
   1. Tokens
   -------------------------------------------------------------------------- */

:root {
  /* Surfaces — a graphite stack. Every plane is grey; nothing is white. */
  --bg-base:        #1b1c1f;   /* page canvas                          */
  --bg-surface:     #232427;   /* cards, tables, header                */
  --bg-sunken:      #2b2c30;   /* inset boxes: wallet, stats           */
  --bg-raised:      #26272b;   /* inputs, table headers                */
  --bg-hover:       #323338;   /* hover state                          */
  --bg-inverse:     #ffffff;   /* primary buttons, icon plates         */

  /* Ink — white on graphite */
  --text-primary:   #ffffff;
  --text-secondary: #b6b9c0;
  --text-tertiary:  #878b94;
  --text-inverse:   #16171a;

  /* Lines */
  --border:         rgba(255, 255, 255, 0.11);
  --border-strong:  rgba(255, 255, 255, 0.20);
  --border-subtle:  rgba(255, 255, 255, 0.07);

  /* Status — readable tints on graphite */
  --success:        #4ade80;
  --success-bg:     rgba(74, 222, 128, 0.14);
  --warning:        #fbbf24;
  --warning-bg:     rgba(251, 191, 36, 0.14);
  --danger:         #fb7185;
  --danger-bg:      rgba(251, 113, 133, 0.14);
  /* #fb7185 is sized for icons and rules, where 3:1 is the bar. As body-sized
     TEXT on a red-tinted panel it measures 4.20:1 on --bg-surface and 3.77:1
     on --bg-sunken — both under 4.5. This variant clears it on every plane in
     the palette (worst 5.40:1) while staying unmistakably the same red. Same
     reasoning as --accent-text above. */
  --danger-text:    #fca5b3;
  --danger-bg-strong: rgba(251, 113, 133, 0.20);
  --info:           #93b4ff;
  --info-bg:        rgba(147, 180, 255, 0.14);

  /* Accent — a single violet carries every primary action and active state.
     Used sparingly: interactive and important, never decorative. */
  --accent:         #7b6cf6;
  /* The accent, lightened for use AS TEXT.
     #7b6cf6 is sized for a filled button, where white sits on top of it. Set
     as a foreground on our lightest panel it measures 3.93:1 — under the 4.5
     that body-sized text needs. This variant clears it on every surface in
     the palette while staying unmistakably the same hue, so links and
     accented values stay readable without altering any button. */
  --accent-text:    #8f83f8;
  --accent-hover:   #8c7eff;
  --accent-soft:    rgba(123, 108, 246, 0.16);
  --accent-line:    rgba(123, 108, 246, 0.42);
  --accent-ink:     #ffffff;

  /* Money reads in gold wherever a figure represents currency. */
  --gold:           #f2b544;
  --gold-soft:      rgba(242, 181, 68, 0.14);

  /* Chrome */
  --impersonation-height: 42px;

  /* Focus */
  --focus-ring:     var(--accent);

  /* Typography */
  --font-display: 'Outfit', 'Segoe UI', system-ui, sans-serif;
  --font-body: 'Inter', 'Segoe UI', system-ui, -apple-system, sans-serif;
  /* The release sheet's title face, taken from the reference design, with the
     same fallback chain it specified. */
  --font-title: 'Poppins', Helvetica, Arial, sans-serif;

  /* Geometry */
  --radius-xs: 4px;
  --radius-sm: 8px;
  --radius-md: 12px;
  --radius-lg: 18px;
  --radius-pill: 999px;

  /* Elevation */
  --shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.35);
  --shadow-md: 0 8px 24px rgba(0, 0, 0, 0.38);
  --shadow-lg: 0 24px 56px rgba(0, 0, 0, 0.52);

  /* Rhythm */
  --header-height: 68px;
  --content-max: 1180px;

  --ease: cubic-bezier(0.2, 0.8, 0.3, 1);
  --speed: 160ms;

  color-scheme: dark;
}

/* --------------------------------------------------------------------------
   2. Reset & base
   -------------------------------------------------------------------------- */

*,
*::before,
*::after { box-sizing: border-box; }

* { margin: 0; padding: 0; }

html {
  -webkit-text-size-adjust: 100%;
  scroll-behavior: smooth;
  /* Reserve the scrollbar track at all times so a page that gains or loses
     it does not shift horizontally under the fixed header. */
  scrollbar-gutter: stable;
}

body {
  min-height: 100vh;
  background: var(--bg-base);
  color: var(--text-primary);
  font-family: var(--font-body);
  font-size: 15px;
  line-height: 1.6;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
}

h1, h2, h3, h4, h5 {
  font-family: var(--font-display);
  font-weight: 600;
  line-height: 1.15;
  letter-spacing: -0.02em;
  color: var(--text-primary);
}

h1 { font-size: clamp(1.7rem, 3vw, 2.15rem); }
h2 { font-size: 1.3rem; }
h3 { font-size: 1.05rem; }
h4 { font-size: 0.95rem; }

p { color: var(--text-secondary); }

a {
  color: inherit;
  text-decoration: none;
  transition: color var(--speed) var(--ease);
}

a:hover { color: var(--text-primary); }

img { max-width: 100%; display: block; }

svg { flex-shrink: 0; }

button, input, select, textarea {
  font: inherit;
  color: inherit;
}

button { background: none; border: none; cursor: pointer; }

hr { border: none; border-top: 1px solid var(--border); }

::selection {
  background: var(--text-primary);
  color: var(--bg-base);
}

:focus-visible {
  outline: 2px solid var(--focus-ring);
  outline-offset: 2px;
  border-radius: var(--radius-xs);
}

/* Scrollbars, tuned to the theme rather than the OS */
* { scrollbar-width: thin; scrollbar-color: var(--border-strong) transparent; }
::-webkit-scrollbar { width: 10px; height: 10px; }
::-webkit-scrollbar-track { background: transparent; }
::-webkit-scrollbar-thumb {
  background: var(--border-strong);
  border-radius: var(--radius-pill);
  border: 3px solid var(--bg-base);
}

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

/* --------------------------------------------------------------------------
   3. Icons
   --------------------------------------------------------------------------
   Every icon is a monochrome line glyph inheriting currentColor. Where an
   icon needs emphasis it sits inside an .icon-tile — a black plate that keeps
   the icon itself colourless.
   -------------------------------------------------------------------------- */

.icon {
  width: 1.125em;
  height: 1.125em;
  fill: none;
  stroke: currentColor;
  stroke-width: 1.6;
  stroke-linecap: round;
  stroke-linejoin: round;
  flex-shrink: 0;
}

.icon-sm { width: 0.95em; height: 0.95em; }

.icon-tile {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 44px;
  height: 44px;
  border-radius: var(--radius-md);
  background: #131318;
  border: 1px solid var(--border);
  color: var(--text-primary);
  flex-shrink: 0;
}

.icon-tile .icon { width: 20px; height: 20px; stroke-width: 1.5; }

.icon-tile-lg { width: 56px; height: 56px; border-radius: var(--radius-lg); }
.icon-tile-lg .icon { width: 24px; height: 24px; }

/* --------------------------------------------------------------------------
   4. Layout primitives
   -------------------------------------------------------------------------- */

.container {
  width: 100%;
  max-width: var(--content-max);
  margin-inline: auto;
  padding-inline: 24px;
}

/* `.main-content` lived here. It wrapped the landing page's stacked
   sections and nothing else — every other screen uses `.panel-layout` — so
   it went with that page. */

/* Block flow, so a full-width band can simply be a child. Nothing here uses
   viewport units for width: `100vw` counts the scrollbar while `100%` does
   not, and mixing them is what pushed the page into horizontal overflow and
   left a gap beside the fixed header. */
.panel-layout {
  display: block;
  min-height: calc(100vh - var(--header-height));
}

.panel-content {
  width: 100%;
  max-width: var(--content-max);
  margin-inline: auto;
  padding: 32px 24px 72px;
  min-width: 0;
}

.stack { display: flex; flex-direction: column; gap: 20px; }
.row { display: flex; align-items: center; gap: 12px; }
.row-between { display: flex; align-items: center; justify-content: space-between; gap: 16px; flex-wrap: wrap; }

/* --------------------------------------------------------------------------
   5. Header  ->  public/css/header.css
   --------------------------------------------------------------------------
   The header, the impersonation bar, the nav and its dropdowns, the account
   controls, the panel switcher, the mobile drawer and its scrim all moved to
   their own stylesheet, linked after this one from partials/head.ejs.

   Nothing header-shaped belongs here any more. A rule for it in this file
   loads BEFORE header.css and would be silently overridden - which is worse
   than not existing, because it reads as though it works.
   -------------------------------------------------------------------------- */

/* --------------------------------------------------------------------------
   7. Buttons
   -------------------------------------------------------------------------- */

.btn {
  --btn-h: 40px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 8px;
  height: var(--btn-h);
  padding: 0 18px;
  border: 1px solid transparent;
  border-radius: var(--radius-sm);
  background: var(--bg-raised);
  color: var(--text-primary);
  font-family: var(--font-body);
  font-size: 0.86rem;
  font-weight: 550;
  letter-spacing: -0.005em;
  white-space: nowrap;
  cursor: pointer;
  transition: background var(--speed) var(--ease), border-color var(--speed) var(--ease),
              color var(--speed) var(--ease), opacity var(--speed) var(--ease);
}

.btn:hover { background: var(--bg-hover); }

.btn:disabled,
.btn[aria-disabled='true'] { opacity: 0.5; cursor: not-allowed; }

.btn .icon { width: 16px; height: 16px; }

.btn-primary {
  background: var(--accent);
  color: var(--accent-ink);
  border-color: var(--accent);
}

.btn-primary:hover { background: var(--accent-hover); border-color: var(--accent-hover); }

.btn-outline {
  background: transparent;
  border-color: var(--border-strong);
  color: var(--text-primary);
}

.btn-outline:hover { background: var(--bg-hover); border-color: var(--text-tertiary); }

.btn-ghost { background: transparent; color: var(--text-secondary); }
.btn-ghost:hover { background: var(--bg-hover); color: var(--text-primary); }

.btn-danger { background: var(--danger-bg); color: var(--danger); }
.btn-danger:hover { background: var(--danger-bg); opacity: 0.8; }

.btn-warning { background: var(--warning-bg); color: var(--warning); }
.btn-warning:hover { background: var(--warning-bg); opacity: 0.8; }

.btn-sm { --btn-h: 32px; padding: 0 12px; font-size: 0.78rem; }
.btn-lg { --btn-h: 48px; padding: 0 26px; font-size: 0.95rem; }
.btn-full { width: 100%; }

.btn-loader { display: none; }
.btn.is-loading .btn-text { display: none; }
.btn.is-loading .btn-loader { display: inline; }

/* --------------------------------------------------------------------------
   8. Forms
   -------------------------------------------------------------------------- */

.form-group { margin-bottom: 18px; }

.form-row {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
  gap: 0 18px;
}

.form-label {
  display: block;
  margin-bottom: 7px;
  font-size: 0.78rem;
  font-weight: 550;
  color: var(--text-secondary);
}

.form-input {
  width: 100%;
  min-height: 42px;
  padding: 10px 13px;
  background: var(--bg-raised);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  color: var(--text-primary);
  font-size: 0.9rem;
  transition: border-color var(--speed) var(--ease), background var(--speed) var(--ease);
}

.form-input::placeholder { color: var(--text-tertiary); }

.form-input:hover:not(:disabled) { border-color: var(--border-strong); }

.form-input:focus {
  outline: none;
  border-color: var(--accent);
  background: var(--bg-surface);
  box-shadow: 0 0 0 3px var(--accent-soft);
}

.form-input:disabled {
  opacity: 0.55;
  cursor: not-allowed;
  background: var(--bg-sunken);
}

.form-input.is-invalid { border-color: var(--danger); }

.form-textarea { min-height: 96px; resize: vertical; line-height: 1.55; }

/* A date field takes a keyboard AND opens a calendar, so it is dressed as
   both: an ordinary text caret for typing, and a pointer over the indicator
   that opens the picker.

   The caret and text selection were suppressed here while typing was blocked.
   They are back, because the field accepts typing again and a control that
   takes a keyboard has to look like it does. */
input[type='date'].form-input { cursor: text; }

/* The dates offered while typing (initDateFields in main.js).

   Anchored to the field's own wrapper rather than the page, so it follows the
   input on scroll and resize with no measuring and no reflow listener. It sits
   above the form but below a modal: this is a list attached to one field, not
   a layer over the page. */
.date-suggest {
  position: absolute;
  z-index: 40;
  top: calc(100% + 4px);
  left: 0;
  right: 0;
  max-height: 232px;
  overflow-y: auto;
  margin: 0;
  padding: 5px;
  list-style: none;
  background: var(--bg-surface);
  border: 1px solid var(--border-strong);
  border-radius: var(--radius-sm);
  box-shadow: 0 12px 32px rgba(0, 0, 0, 0.45);
}

.date-suggest-item {
  padding: 9px 11px;
  border-radius: var(--radius-sm);
  font-size: 0.86rem;
  color: var(--text-primary);
  cursor: pointer;
  white-space: nowrap;
}

.date-suggest-item:hover,
.date-suggest-item.is-active {
  background: var(--accent);
  color: var(--accent-ink);
}

input[type='date'].form-input::-webkit-calendar-picker-indicator {
  cursor: pointer;
  filter: invert(1);
  opacity: 0.55;
}

input[type='date'].form-input:hover::-webkit-calendar-picker-indicator { opacity: 0.9; }

select.form-input {
  appearance: none;
  padding-right: 36px;
  background-image: url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%239b9b9f' stroke-width='2' stroke-linecap='round'%3E%3Cpath d='M6 9l6 6 6-6'/%3E%3C/svg%3E");
  background-repeat: no-repeat;
  background-position: right 12px center;
  background-size: 15px;
  cursor: pointer;
}

select.form-input option { background: var(--bg-surface); color: var(--text-primary); }

.form-hint {
  display: block;
  margin-top: 6px;
  font-size: 0.74rem;
  color: var(--text-tertiary);
}

/* Checkboxes & radios */
.choice {
  display: inline-flex;
  align-items: center;
  gap: 9px;
  cursor: pointer;
  font-size: 0.86rem;
  color: var(--text-primary);
}

.choice input[type='checkbox'],
.choice input[type='radio'] {
  width: 17px;
  height: 17px;
  accent-color: var(--accent);
  cursor: pointer;
}

.choice-row { display: flex; flex-wrap: wrap; gap: 20px; padding-top: 4px; }

/* Toggle switch */
.switch { position: relative; display: inline-block; width: 40px; height: 22px; }
.switch input { position: absolute; opacity: 0; width: 100%; height: 100%; margin: 0; cursor: pointer; }

.slider {
  position: absolute;
  inset: 0;
  border-radius: var(--radius-pill);
  background: var(--bg-hover);
  border: 1px solid var(--border);
  transition: background var(--speed) var(--ease);
  pointer-events: none;
}

.slider::before {
  content: '';
  position: absolute;
  left: 3px;
  top: 50%;
  width: 14px;
  height: 14px;
  border-radius: 50%;
  background: var(--text-secondary);
  transform: translateY(-50%);
  transition: transform var(--speed) var(--ease), background var(--speed) var(--ease);
}

.switch input:checked + .slider { background: var(--accent); border-color: var(--accent); }
.switch input:checked + .slider::before { transform: translate(18px, -50%); background: var(--accent-ink); }
.switch input:disabled + .slider { opacity: 0.5; }
.switch input:disabled { cursor: not-allowed; }

/* Cards that wrap forms */
.form-card {
  padding: 26px;
  margin-bottom: 20px;
  background: var(--bg-surface);
  border: 1px solid var(--border);
  border-radius: var(--radius-lg);
}

.form-card-title {
  margin-bottom: 18px;
  padding-bottom: 12px;
  border-bottom: 1px solid var(--border-subtle);
  font-size: 0.94rem;
}

.form-card-title:not(:first-child) { margin-top: 28px; }

.form-section { margin-bottom: 32px; }

.form-section-title {
  display: flex;
  align-items: center;
  gap: 11px;
  margin-bottom: 6px;
  font-size: 1rem;
}

.section-number {
  display: grid;
  place-items: center;
  width: 26px;
  height: 26px;
  border-radius: var(--radius-sm);
  background: var(--accent);
  color: var(--accent-ink);
  font-size: 0.76rem;
  font-weight: 700;
  flex-shrink: 0;
}

.form-section-desc {
  margin-bottom: 18px;
  font-size: 0.83rem;
  color: var(--text-secondary);
}

/* File uploads */
.file-upload {
  position: relative;
  padding: 26px 20px;
  border: 1.5px dashed var(--border-strong);
  border-radius: var(--radius-md);
  background: var(--bg-raised);
  text-align: center;
  transition: border-color var(--speed) var(--ease), background var(--speed) var(--ease);
}

.file-upload:hover,
.file-upload.dragover { border-color: var(--accent); background: var(--bg-hover); }
.file-upload.is-invalid { border-color: var(--danger); }

.file-input {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  opacity: 0;
  cursor: pointer;
}

.file-input-hidden { display: none; }

.file-upload-content {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 8px;
  color: var(--text-secondary);
  pointer-events: none;
}

.file-upload-content .icon { width: 26px; height: 26px; color: var(--text-tertiary); }
.file-upload-content span { font-size: 0.86rem; font-weight: 500; }
.file-upload-content small { font-size: 0.74rem; color: var(--text-tertiary); }

.file-preview img {
  max-width: 190px;
  margin-inline: auto;
  border-radius: var(--radius-sm);
  border: 1px solid var(--border);
}

/* The player, and the strip it scrolls in.

   Chrome's built-in controls carry a ~273px minimum that no width or
   min-width on the host can override — it lives in the shadow DOM. Because
   that is a MIN-CONTENT width it propagates outward, and on a 320px phone it
   was forcing the review card to 317px inside a 278px column and taking the
   whole page sideways with it.

   So the player keeps its minimum and the strip around it scrolls instead.
   At any normal width there is nothing to scroll; at the very narrowest the
   controls stay whole and reachable rather than being clipped. */
audio {
  width: 100%;
  height: 38px;
}

/* --------------------------------------------------------------------------
   9. Page furniture
   -------------------------------------------------------------------------- */

.page-header {
  display: flex;
  align-items: flex-end;
  justify-content: space-between;
  gap: 18px;
  flex-wrap: wrap;
  margin-bottom: 28px;
  padding-bottom: 20px;
  border-bottom: 1px solid var(--border-subtle);
}

.page-header p { margin-top: 6px; font-size: 0.88rem; }

.page-eyebrow {
  display: block;
  margin-bottom: 6px;
  font-size: 0.7rem;
  font-weight: 600;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--accent-text);
}

.dashboard-section { margin-top: 36px; }

.section-header-inline {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 14px;
  flex-wrap: wrap;
  margin-bottom: 18px;
}

.card {
  background: var(--bg-surface);
  border: 1px solid var(--border);
  border-radius: var(--radius-lg);
  padding: 22px;
}

/* Stacked cards need a gap between them.
   -------------------------------------------------------------------------
   .card carries no bottom margin, unlike .form-card, because it is very often
   a grid child where the grid's own gap does the spacing — adding a margin
   there would double it. But two cards stacked directly in the main column
   had nothing between them at all and met edge to edge, which reads as one
   card with a stray line through it.

   An adjacent-sibling rule rather than a blanket margin: it fires only when
   one card-like block directly follows another as siblings in the main
   column, so a card inside a grid is untouched and no trailing margin is
   added after the last one. */
.panel-content > .card + .card,
.panel-content > .card + .form-card,
.panel-content > .form-card + .card {
  margin-top: 20px;
}

/* A card's header: icon tile, heading, description.
   -------------------------------------------------------------------------
   align-items: flex-start, NOT center. The tile is 44px and a heading line is
   about 21px, so centring the tile against a two- or three-line text block
   drops it 10-21px below the heading it belongs to — measured at +20.9px on
   the change-password header, where it floated between the heading and the
   second line of the description looking like it belonged to neither.

   The small top margin is optical rather than arithmetic: a text box carries
   a few pixels of leading above the glyphs, so a tile flush with the box's
   top edge reads as sitting slightly high. */
.card-head {
  display: flex;
  align-items: flex-start;
  gap: 12px;
}

.card-head > .icon-tile { margin-top: 2px; }
.card-head h3 { margin: 0 0 4px; }
.card-head p { margin: 0; }

/* Filters */
.filter-bar {
  display: flex;
  gap: 6px;
  flex-wrap: wrap;
  margin-bottom: 22px;
  padding: 5px;
  background: var(--bg-sunken);
  border: 1px solid var(--border-subtle);
  border-radius: var(--radius-md);
  width: fit-content;
  max-width: 100%;
}

.filter-btn {
  padding: 7px 15px;
  border-radius: var(--radius-sm);
  font-size: 0.81rem;
  font-weight: 500;
  color: var(--text-secondary);
  transition: background var(--speed) var(--ease), color var(--speed) var(--ease);
}

.filter-btn:hover { background: var(--bg-hover); color: var(--text-primary); }
.filter-btn.active { background: var(--accent); color: var(--accent-ink); }

/* Tabs */
.tabs {
  display: flex;
  gap: 4px;
  margin-bottom: 24px;
  border-bottom: 1px solid var(--border);
}

.tab-content { display: none; }
.tab-content.active { display: block; }

/* --------------------------------------------------------------------------
   10. Tables
   -------------------------------------------------------------------------- */

.table-responsive {
  width: 100%;
  overflow-x: auto;
  background: var(--bg-surface);
  border: 1px solid var(--border);
  border-radius: var(--radius-lg);
}

.data-table {
  width: 100%;
  border-collapse: collapse;
  font-size: 0.86rem;
  min-width: 560px;
}

.data-table th {
  padding: 13px 18px;
  background: var(--bg-raised);
  border-bottom: 1px solid var(--border);
  font-size: 0.7rem;
  font-weight: 600;
  letter-spacing: 0.09em;
  text-transform: uppercase;
  color: var(--text-tertiary);
  text-align: left;
  white-space: nowrap;
}

.data-table td {
  padding: 14px 18px;
  border-bottom: 1px solid var(--border-subtle);
  color: var(--text-secondary);
  vertical-align: middle;
}

.data-table tbody tr:last-child td { border-bottom: none; }
.data-table tbody tr:hover { background: var(--bg-raised); }
.data-table td strong { color: var(--text-primary); font-weight: 600; }

.cell-nowrap { white-space: nowrap; }

.table-link {
  color: var(--accent-text);
  font-weight: 550;
  text-decoration: underline;
  text-underline-offset: 3px;
  text-decoration-color: var(--accent-line);
}

.table-link:hover { color: var(--accent-hover); text-decoration-color: currentColor; }

.numeric { font-variant-numeric: tabular-nums; }
.amount { font-family: var(--font-display); font-weight: 600; color: var(--text-primary); font-variant-numeric: tabular-nums; }

/* --------------------------------------------------------------------------
   11. Status
   -------------------------------------------------------------------------- */

.status-badge {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 4px 11px;
  border-radius: var(--radius-pill);
  font-size: 0.72rem;
  font-weight: 600;
  /* The labels arrive already cased (lib/statusLabels.js). `capitalize` was
     here to dress up raw values like "pending" and would now turn "In review"
     into "In Review". */
  text-transform: none;
  white-space: nowrap;
  background: var(--bg-hover);
  color: var(--text-secondary);
  border: 1px solid var(--border-subtle);
}

.status-badge .icon { width: 12px; height: 12px; }

.status-approved,
.status-completed,
.status-active,
.status-live    { background: var(--success-bg); color: var(--success); border-color: transparent; }

.status-pending,
.status-expired { background: var(--warning-bg); color: var(--warning); border-color: transparent; }

/* `.status-suspended` is gone: the word is not shown anywhere on this
   platform any more (see lib/subscription.js). An account whose access is
   paused reads "Expired", in red.

   Deliberately NOT reusing `.status-expired` above — that one is amber and
   belongs to the song/request status set, where "expired" means a deadline
   passed and nothing is wrong. An account with no live plan is a different
   thing on a different axis, and two states in one list sharing a colour
   while meaning different things is worse than one extra class. */
.status-rejected,
.status-takedown,
.status-plan-expired { background: var(--danger-bg); color: var(--danger); border-color: transparent; }

.status-reviewed { background: var(--info-bg); color: var(--info); border-color: transparent; }

/* A row an administrator has switched off — an unpublished plan, a disabled
   store. Neutral rather than red: nothing has gone wrong, it is simply not in
   use, and colouring it as a failure makes a settings list read as alarming. */
.status-off {
  background: var(--bg-raised);
  color: var(--text-tertiary);
  border-color: transparent;
}

.status-lg { padding: 6px 15px; font-size: 0.78rem; }

.status-dot {
  display: inline-block;
  width: 7px;
  height: 7px;
  border-radius: 50%;
  background: currentColor;
  flex-shrink: 0;
}

.pulse { animation: pulse 1.8s ease-in-out infinite; }

@keyframes pulse {
  0%, 100% { opacity: 1; }
  50% { opacity: 0.35; }
}
.text-faint { color: var(--text-tertiary); }

/* --------------------------------------------------------------------------
   12. Feedback: flash, alerts, empty states, modals
   -------------------------------------------------------------------------- */

.flash-message {
  display: flex;
  align-items: center;
  gap: 11px;
  max-width: var(--content-max);
  margin: 0 auto 20px;
  padding: 13px 16px;
  border: 1px solid var(--border);
  border-radius: var(--radius-md);
  background: var(--bg-surface);
  font-size: 0.86rem;
  color: var(--text-primary);
  box-shadow: var(--shadow-sm);
  transition: opacity 240ms var(--ease), transform 240ms var(--ease);
}

.flash-message .flash-icon {
  display: grid;
  place-items: center;
  width: 22px;
  height: 22px;
  border-radius: 50%;
  flex-shrink: 0;
}

.flash-message .flash-icon .icon { width: 13px; height: 13px; stroke-width: 2.2; }

.flash-error { border-color: rgba(248, 113, 113, 0.32); }
.flash-error .flash-icon { background: var(--danger-bg); color: var(--danger); }

.alert {
  display: flex;
  gap: 12px;
  padding: 15px 17px;
  margin-bottom: 20px;
  border: 1px solid var(--border);
  border-radius: var(--radius-md);
  background: var(--bg-raised);
  font-size: 0.86rem;
  color: var(--text-secondary);
}

.alert strong { color: var(--text-primary); }
.alert .icon { width: 18px; height: 18px; margin-top: 2px; }

.alert-error { border-color: rgba(248, 113, 113, 0.3); background: var(--danger-bg); }
.alert-error .icon { color: var(--danger); }

.alert-info .icon { color: var(--info); }

.empty-state {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 6px;
  padding: 64px 24px;
  text-align: center;
  background: var(--bg-surface);
  border: 1px dashed var(--border);
  border-radius: var(--radius-lg);
}

.empty-state .icon-tile { margin-bottom: 12px; }
.empty-state h3 { font-size: 1rem; }
.empty-state p { font-size: 0.86rem; max-width: 380px; }
.empty-state .btn { margin-top: 14px; }

.empty-state-sm {
  padding: 30px 20px;
  text-align: center;
  font-size: 0.85rem;
  color: var(--text-tertiary);
  background: var(--bg-raised);
  border: 1px dashed var(--border-subtle);
  border-radius: var(--radius-md);
}

/* The page does not scroll behind an open popup. openModal(), openCheckout()
   and openRenewPrompt() all set this class; until now nothing acted on it, so
   a modal on a phone floated over a page that still moved under the finger. */
body.modal-open { overflow: hidden; }

/* Modals */
.modal-overlay {
  position: fixed;
  inset: 0;
  z-index: 500;
  display: none;
  align-items: center;
  justify-content: center;
  padding: 20px;
  background: rgba(0, 0, 0, 0.62);
  opacity: 0;
  transition: opacity 200ms var(--ease);
}

.modal-overlay.show { display: flex; opacity: 1; }

.modal {
  width: 100%;
  max-width: 420px;
  padding: 30px;
  background: var(--bg-surface);
  border: 1px solid var(--border);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-lg);
  transform: translateY(10px) scale(0.99);
  transition: transform 220ms var(--ease);
  /* A ceiling, so a long message scrolls inside the panel rather than growing
     past the top and bottom of the window where the buttons cannot be reached.
     40px leaves the overlay's padding visible, so it still reads as a card on
     a backdrop rather than a full-screen takeover. */
  max-height: calc(100vh - 40px);
  overflow-y: auto;
}

.modal-overlay.show .modal { transform: translateY(0) scale(1); }

.modal-center { text-align: center; }
.modal-center .icon-tile { margin: 0 auto 16px; }
.modal h3 { margin-bottom: 9px; font-size: 1.08rem; }
.modal p { font-size: 0.88rem; line-height: 1.6; }
.modal-actions { display: flex; gap: 10px; margin-top: 24px; }
.modal-actions .btn { flex: 1; }

/* --------------------------------------------------------------------------
   Nothing escapes the box
   --------------------------------------------------------------------------
   These popups report what a server said, and servers say things with no
   spaces in them: a host key fingerprint, a file path, an API error, a URL.
   A 46-character token cannot break at a space because there is no space, so
   by default it runs straight out through the side of the panel and off the
   screen — which is what an SFTP fingerprint did.

   `anywhere` rather than `break-word`: it also lets the element report a small
   min-content width, so the same long token cannot force a flex or grid parent
   wider than its container either. Ordinary prose is unaffected — a break is
   only taken when a word genuinely does not fit.

   Applied to .modal so it covers every popup in the product, not just the one
   that was reported. -------------------------------------------------------- */

/*
 * Set on the panel, not on a list of tags.
 *
 * overflow-wrap inherits, so one declaration covers every popup in the product
 * and every element inside one — including the parts of the renew, checkout
 * and upload-success panels that are not paragraphs, and anything added later.
 * Listing tags would have fixed the fingerprint and left the next long string
 * to be discovered the same way.
 *
 * Buttons are unaffected: .btn sets white-space: nowrap, which keeps a label
 * on one line regardless of what it inherits.
 */
.modal { overflow-wrap: anywhere; }

/* --------------------------------------------------------------------------
   Dialog — the single centred popup every panel speaks through
   --------------------------------------------------------------------------
   One component for confirmations, results and errors, on the admin panel,
   the artist panel and every form in between. It replaces the browser's own
   confirm()/alert(), which cannot be styled, pins itself to the top of the
   window, and announces the bare host name to the reader before it says
   anything useful.

   Built on .modal-overlay/.modal above so the backdrop, the entrance
   transition, the click-outside and the Escape handling are all shared
   rather than reimplemented. */

.dialog { max-width: 440px; }

.dialog-icon {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 52px;
  height: 52px;
  margin: 0 auto 18px;
  border-radius: 50%;
  background: var(--bg-hover);
  color: var(--text-secondary);
}

.dialog-icon .icon { width: 24px; height: 24px; stroke-width: 2; }
.dialog-error   .dialog-icon,
.dialog-danger  .dialog-icon { background: var(--danger-bg);  color: var(--danger); }
.dialog-warning .dialog-icon { background: var(--warning-bg); color: var(--warning); }
.dialog-info    .dialog-icon { background: var(--info-bg);    color: var(--info); }

.dialog h3 {
  margin-bottom: 10px;
  font-size: 1.16rem;
  font-weight: 650;
  letter-spacing: -0.01em;
  color: var(--text-primary);
}

.dialog p {
  margin: 0;
  color: var(--text-secondary);
  font-size: 0.9rem;
  line-height: 1.65;
  /* Confirmation copy is written as paragraphs; keep the author's breaks. */
  white-space: pre-line;
}

.dialog-note {
  margin-top: 16px;
  padding: 12px 14px;
  border-radius: var(--radius-sm);
  background: var(--bg-raised);
  border: 1px solid var(--border-subtle);
  font-size: 0.83rem;
  line-height: 1.6;
  color: var(--text-secondary);
  text-align: left;
}

.dialog .modal-actions { margin-top: 26px; }
/* A long button label wraps inside its button rather than pushing it out of
   the dialog, which is how the Install button ended up half hidden. */
.dialog .modal-actions .btn { min-width: 0; white-space: normal; }

/* `hidden` has to beat a component's own display value. The attribute is
   only `display: none` in the browser's default stylesheet, which any author
   rule setting display outranks — and .btn sets `inline-flex`, so hiding the
   cancel button on a message with nothing to cancel silently did nothing. */
[hidden] { display: none !important; }

/* A single acknowledgement should not stretch the full width of the card. */
.dialog .modal-actions.is-single { justify-content: center; }
.dialog .modal-actions.is-single .btn { flex: 0 1 190px; }

.modal-list { margin: 20px 0 4px; text-align: left; }

.modal-list-item {
  display: flex;
  justify-content: space-between;
  gap: 16px;
  padding: 11px 0;
  border-bottom: 1px solid var(--border-subtle);
  font-size: 0.86rem;
}

.modal-list-item:last-child { border-bottom: none; }
.modal-list-item dt { color: var(--text-secondary); }
.modal-list-item dd { color: var(--text-primary); font-weight: 550; }

/* Full-screen progress overlay */
.busy-overlay {
  position: fixed;
  inset: 0;
  z-index: 900;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 22px;
  background: var(--bg-base);
  text-align: center;
}

.spinner {
  width: 34px;
  height: 34px;
  border: 2px solid var(--border);
  border-top-color: var(--text-primary);
  border-radius: 50%;
  animation: spin 0.8s linear infinite;
}

@keyframes spin { to { transform: rotate(360deg); } }

.busy-overlay h2 { font-size: 1.1rem; }
.busy-overlay p { font-size: 0.85rem; }

.tooltip-text {
  position: absolute;
  bottom: calc(100% + 9px);
  right: 0;
  /* 264px before. These hold two short sentences, and at the old width that
     was four lines — long enough to look like something to read rather than
     something to glance at. The min() keeps it inside a narrow screen, where
     initFloatingTooltips() would otherwise have to clamp a box wider than the
     viewport. */
  width: min(340px, calc(100vw - 32px));
  padding: 12px 14px;
  background: var(--bg-raised);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  box-shadow: var(--shadow-md);
  font-size: 0.79rem;
  line-height: 1.5;
  color: var(--text-secondary);
  text-align: left;
  opacity: 0;
  visibility: hidden;
  transition: opacity var(--speed) var(--ease);
  z-index: 60;
  pointer-events: none;
}

/* Any element carrying [data-tooltip-float] is a tooltip anchor, and opens on
   the three gestures a popup is reached by: hover, keyboard focus, and a tap
   (initFloatingTooltips() adds .is-open — a touch screen has no hover, and on
   iOS a tap on a non-interactive element does not necessarily focus it, so
   neither of the first two can be relied on alone).

   The anchor is positioned so the popup has something to hang from before the
   script upgrades it to viewport coordinates: without JS the tooltip still
   opens, just at its CSS position rather than a measured one. */
[data-tooltip-float] {
  position: relative;
  display: inline-flex;
  align-items: center;
  /* Says "there is more here" without borrowing the pointer that belongs to
     things you can actually click. */
  cursor: help;
}

[data-tooltip-float]:hover > .tooltip-text,
[data-tooltip-float]:focus > .tooltip-text,
[data-tooltip-float]:focus-within > .tooltip-text,
[data-tooltip-float].is-open > .tooltip-text { opacity: 1; visibility: visible; }

/* A supporting line inside a tooltip — the admin's own note on a payout,
   under the standing explanation it qualifies. Ruled off rather than merely
   spaced, because the two halves come from different authors. */
.tooltip-note {
  display: block;
  margin-top: 8px;
  padding-top: 8px;
  border-top: 1px solid var(--border-subtle);
}

.tooltip-note strong { display: block; color: var(--text-primary); }

/* Re-anchored to the viewport by initFloatingTooltips(). A tooltip inside a
   horizontally scrolling table is clipped on both axes — `overflow-x: auto`
   forces `overflow-y` to compute to `auto` too — and `.data-table` carries a
   560px min-width, so those tables really do scroll on a phone. Escaping the
   container is the only fix; JS supplies the coordinates. */
.tooltip-text.is-floating {
  position: fixed;
  bottom: auto;
  right: auto;
}

/* --------------------------------------------------------------------------
   13. Auth & install
   -------------------------------------------------------------------------- */

.auth-page {
  display: flex;
  align-items: center;
  justify-content: center;
  min-height: 100vh;
  padding: 40px 20px;
  background: var(--bg-base);
}

/* --------------------------------------------------------------------------
   Cinematic sign-in background
   --------------------------------------------------------------------------
   Layered so the page is finished at every stage of loading: the gradient
   paints with the stylesheet, the poster replaces it the moment it decodes,
   and the video — if one is installed and worth the bytes — fades in over
   both. Nothing here waits on a network request to look deliberate. */

.auth-cinematic { position: relative; overflow: hidden; }

.auth-bg {
  position: fixed;
  inset: 0;
  z-index: 0;
  pointer-events: none;
}

/* Two large, slow, offset radial washes. Drifting them in opposite
   directions keeps the motion from reading as a single sliding sheet. */
.auth-bg-aurora {
  position: absolute;
  inset: -20%;
  background:
    radial-gradient(38% 46% at 22% 26%, rgba(124, 58, 237, 0.55), transparent 70%),
    radial-gradient(34% 40% at 78% 30%, rgba(37, 99, 235, 0.42), transparent 72%),
    radial-gradient(46% 50% at 60% 88%, rgba(88, 28, 190, 0.40), transparent 74%);
  filter: blur(28px);
  animation: aurora-drift 26s ease-in-out infinite alternate;
}

@keyframes aurora-drift {
  0%   { transform: translate3d(-3%, -2%, 0) scale(1.04); }
  50%  { transform: translate3d(2%, 3%, 0) scale(1.10); }
  100% { transform: translate3d(4%, -3%, 0) scale(1.05); }
}

.auth-bg-video,
.auth-bg-still,
.auth-bg-poster {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  background-size: cover;
  background-position: center;
}

/* The backdrop is at its configured strength from the first frame.
 *
 * It used to start at opacity 0 and fade up over a second once the video could
 * paint — which meant every visit opened on a wash of dark that resolved into
 * the footage, and the loop seam did the same thing again every cycle. The
 * fade was hiding a cut; the cut is better than announcing itself twice a
 * minute.
 *
 * --auth-bg-opacity is set per page by the administrator's setting (see
 * views/login.ejs). It falls back to 1, so a page rendered before that
 * setting existed still shows a fully opaque backdrop. */
.auth-bg-video,
.auth-bg-still,
.auth-bg-poster { opacity: var(--auth-bg-opacity, 1); }

/* Darkens the busiest part of the frame and keeps the card readable no
   matter what footage is installed behind it. */
.auth-bg-scrim {
  position: absolute;
  inset: 0;
  background:
    radial-gradient(58% 58% at 50% 45%, rgba(6, 4, 14, 0.34), rgba(6, 4, 14, 0.78) 100%),
    linear-gradient(180deg, rgba(6, 4, 14, 0.55) 0%, rgba(6, 4, 14, 0.72) 100%);
}

/* The card floats above the stack. */
.auth-cinematic .auth-container { position: relative; z-index: 1; }

/* Glass, so whatever is behind reads through without costing legibility.
 *
 * No drop shadow. It was doing nothing on the sign-in screen — a card this
 * translucent over a gradient this busy has no edge for a shadow to fall from
 * — and on the reset screens, which now use this same card, it showed as a
 * grey halo. The blur and the hairline border are what separate the panel from
 * the page; the shadow was a third answer to a question already answered. */
.auth-cinematic .auth-card {
  background: rgba(20, 17, 30, 0.72);
  border-color: rgba(255, 255, 255, 0.10);
  backdrop-filter: blur(18px) saturate(140%);
  -webkit-backdrop-filter: blur(18px) saturate(140%);
}

.auth-cinematic .auth-card .form-input {
  background: rgba(255, 255, 255, 0.06);
  border-color: rgba(255, 255, 255, 0.14);
}

.auth-cinematic .auth-card .form-input:focus {
  background: rgba(255, 255, 255, 0.10);
}

/* Motion is decoration here; anyone who has switched it off gets the same
   picture, still. The video is never even requested — see main.js. */
@media (prefers-reduced-motion: reduce) {
  .auth-bg-aurora { animation: none; }
  /* Nothing to disable here any more: the backdrop no longer transitions.
     The video itself is not started at all under reduced motion — see
     initLoginBackdrop in main.js. */
}

.auth-container { width: 100%; max-width: 424px; }
.install-container { max-width: 720px; }

/* No drop shadow.
 *
 * On the sign-in screen it was invisible anyway — the card sits on a gradient
 * busy enough to swallow it. On the reset screens, which had a flat background
 * until now, the same shadow read as a grey halo around the card and was the
 * most obvious difference between two pages that are meant to look identical.
 * Removing it costs nothing on one page and fixes the other. The border still
 * separates the card from what is behind it. */
.auth-card {
  padding: 38px;
  background: var(--bg-surface);
  border: 1px solid var(--border);
  border-radius: var(--radius-lg);
}

.auth-header { margin-bottom: 28px; text-align: center; }

.auth-header h1 { font-size: 1.5rem; margin-bottom: 6px; }
.auth-subtitle { font-size: 0.87rem; color: var(--text-secondary); }

.logo-large {
  display: inline-grid;
  place-items: center;
  width: 52px;
  height: 52px;
  margin-bottom: 18px;
  border-radius: var(--radius-md);
  background: var(--accent);
  color: var(--accent-ink);
}

.logo-large .icon { width: 26px; height: 26px; stroke-width: 1.6; }

/* An uploaded logo, shown as itself.
 *
 * The accent tile above exists to give the bare music note a shape to sit in.
 * A real logo arrives with its own shape — and usually its own background —
 * so the tile is dropped rather than boxing someone's brand mark in a purple
 * square it was never drawn for. Height-constrained rather than width-, since
 * a wordmark and a square badge want the same cap height and wildly different
 * widths. */
.logo-large.is-image {
  width: auto;
  max-width: 220px;
  height: 52px;
  background: none;
  border-radius: 0;
}

.logo-large.is-image img {
  max-width: 100%;
  max-height: 52px;
  width: auto;
  height: auto;
  object-fit: contain;
}

/* Larger on the sign-in screen alone.
 *
 * That card lost its heading and its subtitle, which were saying what the two
 * fields below already say. The mark is what identifies the page now, so it
 * gets the room they were using — and only there: the reset screens still lead
 * with a heading that has to be read, and a mark that size above it would
 * outweigh the sentence doing the work. */
.login-page .logo-large { height: 74px; margin-bottom: 22px; }
.login-page .logo-large.is-image { max-width: 260px; }
.login-page .logo-large.is-image img { max-height: 74px; }

/* The fallback note keeps its tile, scaled to match. */
.login-page .logo-large:not(.is-image) { width: 74px; }
.login-page .logo-large:not(.is-image) .icon { width: 38px; height: 38px; }

/* The header is only a logo now, so the gap under it is the logo's own. */
.auth-header-brand { margin-bottom: 26px; }

.auth-form .btn { margin-top: 6px; }

.auth-footer {
  margin-top: 24px;
  padding-top: 20px;
  border-top: 1px solid var(--border-subtle);
  text-align: center;
  font-size: 0.85rem;
  color: var(--text-secondary);
}

.auth-footer a { color: var(--text-primary); font-weight: 550; text-decoration: underline; text-underline-offset: 3px; }

/* --------------------------------------------------------------------------
   Auth: inline errors, and the two password-reset screens
   --------------------------------------------------------------------------
   A failed sign-in used to open the centred dialog every other message uses.
   That component is right for "this action had a consequence" and wrong for
   "this field is wrong": it covers the form, it has to be dismissed before
   the value can be corrected, and it reads as a fault rather than as a typo.
   The message now renders in the form, directly under the button — the point
   the eye is already at, because it is the point that was just clicked. */

.form-error {
  display: flex;
  align-items: flex-start;
  gap: 9px;
  margin-top: 16px;
  padding: 12px 14px;
  background: var(--danger-bg);
  border: 1px solid rgba(251, 113, 133, 0.28);
  border-radius: var(--radius-sm);
  font-size: 0.84rem;
  line-height: 1.55;
  /* --danger-text, not --danger: this is body-sized text on a tinted panel,
     where the icon-weight red does not clear 4.5:1. See the palette note. */
  color: var(--danger-text);
  text-align: left;
}

.form-error .icon { width: 16px; height: 16px; margin-top: 1px; flex-shrink: 0; color: var(--danger); }

/* Label on the left, "Forgot password?" on the right, sharing the baseline
   the field is titled by — so the escape hatch is exactly where the trouble
   is, rather than buried in the footer under an unrelated link. */
.label-row {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 12px;
}

.label-link {
  font-size: 0.78rem;
  font-weight: 550;
  color: var(--accent-text);
  margin-bottom: 7px;
}

.label-link:hover { color: var(--text-primary); text-decoration: underline; text-underline-offset: 3px; }

/* The confirmation and expiry screens lead with a glyph instead of the
   wordmark: they are outcomes, and an outcome should say which one it is
   before it says whose product it is. */
.auth-glyph {
  display: inline-grid;
  place-items: center;
  width: 54px;
  height: 54px;
  margin-bottom: 18px;
  border-radius: 50%;
  background: var(--bg-hover);
  color: var(--text-secondary);
}

.auth-glyph .icon { width: 26px; height: 26px; stroke-width: 1.8; }
.auth-glyph-success { background: var(--success-bg); color: var(--success); }
.auth-glyph-warning { background: var(--warning-bg); color: var(--warning); }

.auth-strong { color: var(--text-primary); font-weight: 600; overflow-wrap: anywhere; }

.auth-note {
  margin: 22px 0;
  padding: 15px 17px;
  background: var(--bg-raised);
  border: 1px solid var(--border-subtle);
  border-radius: var(--radius-sm);
  font-size: 0.83rem;
  line-height: 1.6;
  color: var(--text-secondary);
  text-align: left;
}

.auth-note p { margin: 0; }
.auth-note p + p { margin-top: 9px; }
.auth-note-tight { margin-bottom: 0; font-size: 0.79rem; color: var(--text-tertiary); }

.install-success { text-align: center; padding: 20px 0; }
.install-success h2 { margin-bottom: 10px; font-size: 1.35rem; }
.install-success p { margin-bottom: 24px; font-size: 0.89rem; }

/* What an update actually changed, listed by name. Left-aligned inside a
   centred panel because it is a list of identifiers, and centred identifiers
   are hard to scan. */
.install-changes {
  margin: 0 0 24px;
  padding: 14px 16px;
  background: var(--bg-raised);
  border: 1px solid var(--border-subtle);
  border-radius: var(--radius-sm);
  text-align: left;
}

.install-changes p { margin: 0; font-size: 0.83rem; line-height: 1.6; color: var(--text-secondary); }
.install-changes p + p { margin-top: 8px; }
.install-changes strong { color: var(--text-primary); }

.success-icon {
  display: grid;
  place-items: center;
  width: 56px;
  height: 56px;
  margin: 0 auto 20px;
  border-radius: 50%;
  background: var(--success-bg);
  color: var(--success);
}

.success-icon .icon { width: 26px; height: 26px; stroke-width: 2.2; }

.error-card { text-align: center; }

.error-code {
  font-family: var(--font-display);
  font-size: 4rem;
  font-weight: 600;
  letter-spacing: -0.05em;
  line-height: 1;
  color: var(--text-tertiary);
  margin-bottom: 12px;
}

.error-card h1 { font-size: 1.3rem; margin-bottom: 8px; }
.error-card p { margin-bottom: 24px; font-size: 0.88rem; }

/* --------------------------------------------------------------------------
   15. Domain components
   -------------------------------------------------------------------------- */

/* Release rows on "My Releases" */
.tag {
  display: inline-flex;
  align-items: center;
  gap: 7px;
  padding: 5px 12px;
  border: 1px solid var(--border);
  border-radius: var(--radius-pill);
  font-size: 0.76rem;
  font-weight: 500;
  color: var(--text-secondary);
}

.tag .icon { width: 13px; height: 13px; }

/* Every row is its own grid, so labels and values align on one axis across
   both the list and the detail sheet. */
.detail-rows { display: flex; flex-direction: column; }

.detail-rows > div {
  display: grid;
  grid-template-columns: 118px minmax(0, 1fr);
  gap: 16px;
  align-items: center;
  padding: 11px 0;
  border-bottom: 1px solid var(--border-subtle);
  font-size: 0.87rem;
}

.detail-rows > div:last-child { border-bottom: none; }
.detail-rows dt { color: var(--text-tertiary); }

.detail-rows dd {
  color: var(--text-primary);
  font-weight: 500;
  overflow-wrap: anywhere;
}

@media (max-width: 780px) {
  .detail-rows > div { grid-template-columns: 104px minmax(0, 1fr); gap: 12px; }
}

/* Grid and flex items default to min-width: auto, so a track can never be
   narrower than its item's min-content — one wide thing inside a card ends
   up widening the column, then the page. Releasing it here means the CARD
   stays inside the viewport and anything genuinely unshrinkable overflows
   only its own box, where it can be dealt with locally. */
.review-grid > *,
.decision-grid > *,
.detail-split > * { min-width: 0; }

.profile-photo-lg,
.profile-photo-placeholder-lg {
  width: 88px;
  height: 88px;
  border-radius: 50%;
  object-fit: cover;
  border: 1px solid var(--border);
  flex-shrink: 0;
}

.profile-photo-placeholder-lg {
  display: grid;
  place-items: center;
  background: var(--accent);
  color: var(--accent-ink);
  font-family: var(--font-display);
  font-size: 2rem;
  font-weight: 600;
}

/* The .payout-quote block that used to sit under the artist's amount field is
   gone with the breakdown it framed. .payout-quote-total survives on its own
   because the ADMIN payout card still uses it for the "Amount to transfer"
   row — same emphasis, different screen. */
.payout-quote-total dt,
.payout-quote-total dd {
  font-weight: 700;
  color: var(--text-primary);
}

.payout-quote-total dd { font-family: var(--font-display); font-size: 1.05rem; }

.input-with-affix { position: relative; }

.input-affix-btn {
  position: absolute;
  right: 8px;
  top: 50%;
  transform: translateY(-50%);
  display: grid;
  place-items: center;
  width: 30px;
  height: 30px;
  border-radius: var(--radius-xs);
  color: var(--text-tertiary);
}

.input-affix-btn:hover { color: var(--text-primary); background: var(--bg-hover); }
.input-affix-btn .icon { width: 16px; height: 16px; }

.txn-amount {
  font-family: var(--font-display);
  font-size: 1.05rem;
  font-weight: 600;
  font-variant-numeric: tabular-nums;
}

/* Tall panels on short screens (a phone in landscape) must scroll inside
   themselves rather than pushing the confirm button off the viewport. */
.checkout-modal .modal {
  max-height: calc(100vh - 48px);
  overflow-y: auto;
}

/* Notifications page */
.notifications-list { display: flex; flex-direction: column; gap: 8px; }

.notification-list-item {
  display: flex;
  align-items: flex-start;
  gap: 14px;
  padding: 17px 20px;
  background: var(--bg-surface);
  border: 1px solid var(--border);
  border-radius: var(--radius-md);
  transition: border-color var(--speed) var(--ease);
}

.notification-list-item:hover { border-color: var(--border-strong); }
.notification-list-item.unread { background: var(--bg-raised); border-color: var(--border-strong); }
.notification-list-item .notif-dot { margin-top: 7px; }
.notification-list-item .notif-content { flex: 1; }
.notification-list-item .btn { flex-shrink: 0; }

/* --------------------------------------------------------------------------
   15b. Artist dashboard
   --------------------------------------------------------------------------
   A full-bleed warm band carries the wallet, then the page drops onto a
   neutral surface for the rest of the content — the same rhythm as the
   reference design, built from flat colour rather than gradients.
   -------------------------------------------------------------------------- */

/* A direct child of .panel-layout, so it is naturally the full width of the
   page — no viewport-unit arithmetic, no horizontal overflow. */
/* The one gradient in the system, requested for the wallet band. Everything
   else remains flat — keep it that way so this stays a deliberate accent
   rather than the start of a trend. */
.dash-band {
  padding: 30px 24px;
  background:
    linear-gradient(135deg, #4a2a10 0%, #2e1d12 44%, var(--bg-surface) 100%);
  border-bottom: 1px solid var(--border-subtle);
}

/* Tiles sit on the gradient, so they need their own translucent plate rather
   than the opaque grey used elsewhere. */
.dash-band .dash-tile {
  background: rgba(255, 255, 255, 0.06);
  border: 1px solid rgba(255, 255, 255, 0.10);
  backdrop-filter: none;
}

.dash-band .dash-tile:hover { background: rgba(255, 255, 255, 0.09); }

/* On the warm band a violet button clashes, so the primary action inverts. */
.dash-band .btn-primary {
  background: #fff;
  border-color: #fff;
  color: #1a1206;
}

.dash-band .btn-primary:hover { background: rgba(255, 255, 255, 0.88); border-color: transparent; }

.dash-band .btn-outline {
  border-color: rgba(255, 255, 255, 0.28);
  color: #fff;
}

.dash-band .btn-outline:hover { background: rgba(255, 255, 255, 0.12); }

/* Held 48px narrower than --content-max so the tiles line up with the page
   content below, which sits inside .panel-content's 24px gutters. */
.dash-band-inner {
  max-width: calc(var(--content-max) - 48px);
  margin-inline: auto;
}

.dash-band-cards {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: 16px;
  align-items: stretch;
}

/* Equal height regardless of how much text each carries. */
.dash-band-cards > .dash-tile { height: 110%; }

@media (max-width: 1040px) {
  .dash-band-cards { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}

@media (max-width: 680px) {
  .dash-band-cards { grid-template-columns: 1fr; }
}

/* The inset boxes: one step darker than the panel they sit on, with no
   border — the tonal step alone separates them. */
.dash-tile {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 16px;
  padding: 22px 24px;
  background: var(--bg-sunken);
  border-radius: var(--radius-lg);
  min-width: 0;
  transition: background var(--speed) var(--ease);
}

.dash-tile:hover { background: var(--bg-hover); }

.dash-tile-label {
  display: block;
  font-size: 0.74rem;
  font-weight: 600;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--text-tertiary);
  margin-bottom: 8px;
}

.dash-tile-value {
  display: flex;
  align-items: center;
  gap: 10px;
  font-family: var(--font-display);
  font-size: 1.7rem;
  font-weight: 600;
  letter-spacing: 0.005em;
  line-height: 1;
  color: var(--text-primary);
  font-variant-numeric: tabular-nums;
  overflow-wrap: anywhere;
}


.dash-tile .btn { flex-shrink: 0; }

/* The neutral working area beneath the band */
.dash-body { padding-top: 4px; }

/* --------------------------------------------------------------------------
   16. Utilities
   -------------------------------------------------------------------------- */

.visually-hidden {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0 0 0 0);
  white-space: nowrap;
  border: 0;
}
.mt-2 { margin-top: 16px; }
.mb-2 { margin-bottom: 16px; }
.mb-3 { margin-bottom: 24px; }
.hidden { display: none !important; }

@keyframes fadeIn {
  from { opacity: 0; transform: translateY(6px); }
  to { opacity: 1; transform: none; }
}

/* --------------------------------------------------------------------------
   17. Footer
   -------------------------------------------------------------------------- */

.site-footer {
  border-top: 1px solid var(--border);
  background: var(--bg-surface);
  padding: 42px 0 34px;
}

.footer-container {
  max-width: var(--content-max);
  margin-inline: auto;
  padding-inline: 24px;
}

/* Admin-authored markup. Everything below normalises arbitrary HTML into the
   design system so the footer cannot visually break the page, whatever the
   administrator pastes in. */
.footer-content {
  font-size: 0.86rem;
  line-height: 1.65;
  color: var(--text-secondary);
  overflow-wrap: anywhere;
}

.footer-content > *:first-child { margin-top: 0; }
.footer-content > *:last-child { margin-bottom: 0; }

.footer-content p { margin-bottom: 12px; color: var(--text-secondary); }

.footer-content h1,
.footer-content h2,
.footer-content h3,
.footer-content h4,
.footer-content h5,
.footer-content h6 {
  margin: 0 0 12px;
  font-size: 0.8rem;
  font-weight: 600;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--text-tertiary);
}

.footer-content a {
  color: var(--text-primary);
  text-decoration: underline;
  text-underline-offset: 3px;
  text-decoration-color: var(--border-strong);
}

.footer-content a:hover { text-decoration-color: currentColor; }

.footer-content ul,
.footer-content ol { list-style: none; margin-bottom: 16px; }
.footer-content ol { list-style: decimal; padding-left: 18px; }
.footer-content li { margin-bottom: 7px; }

.footer-content img { display: inline-block; max-width: 100%; height: auto; }
.footer-content hr { margin: 22px 0; border-top: 1px solid var(--border-subtle); }

.footer-content table { width: 100%; border-collapse: collapse; margin-bottom: 16px; }
.footer-content th,
.footer-content td { padding: 8px 12px; border-bottom: 1px solid var(--border-subtle); text-align: left; }

.footer-content blockquote {
  margin: 0 0 16px;
  padding-left: 14px;
  border-left: 2px solid var(--border);
  color: var(--text-tertiary);
}

.footer-default { text-align: center; color: var(--text-tertiary); font-size: 0.79rem; }

/* --------------------------------------------------------------------------
   18. Responsive
   -------------------------------------------------------------------------- */

/* Mobile navigation moved to public/css/header.css along with the rest of the
   header - the drawer, its scrim and the breakpoints now live in one file. */

@media (max-width: 640px) {
  body { font-size: 14.5px; }
  .panel-content { padding: 22px 16px 56px; }
  .dash-band { padding: 22px 16px; }
  .dash-tile { padding: 18px; flex-wrap: wrap; }
  .dash-tile-value { font-size: 1.6rem; }
  /* .header-container is not in this list any more: its gutter is fluid and
     already floors at 16px, and header.css loads later, so naming it here
     only looked like it was doing something. */
  .container, .footer-container { padding-inline: 16px; }
  .section { padding: 62px 0; }
  .page-header { align-items: flex-start; flex-direction: column; }

  /* Buttons stack on a narrow screen — and must stop flexing when they do.
   *
   * .modal-actions .btn carries flex: 1 so two buttons share a row evenly.
   * Turning the row into a column turns that same rule on their HEIGHT:
   * flex: 1 is flex-basis: 0, so each button gave up its 40px and collapsed
   * to the height of its own line box. 24px, on the one layout where the
   * target is a thumb rather than a cursor — every confirm, every result,
   * every popup in both panels.
   *
   * flex: none puts the button's own height back in charge; the full width is
   * what stacking was for in the first place. */
  .modal-actions { flex-direction: column; }
  .modal-actions .btn,
  .dialog .modal-actions.is-single .btn { flex: none; width: 100%; }
}

@media print {
  .site-header, .site-footer, .flash-message, .btn { display: none !important; }
  body { background: #fff; color: #000; }
}

/* The `.upgrade-facts` grid that used to sit here — Started / Expires /
   Uploads / Payouts — is gone along with the markup that used it. The plan an
   artist is on is the one fact this page needs to state; a row of dates below
   the plan cards was four more things to read on a screen whose entire job is
   a single choice. */

/* --------------------------------------------------------------------------
   Renewal prompt
   --------------------------------------------------------------------------
   The one popup an artist is meant to act on. Red, and nothing else on it —
   a glyph, the fact, the date, and the way out. Anything more is reading
   material in front of a button.

   The red is a lit edge and a glow AROUND the panel rather than a wash across
   it: the surface stays the same graphite as every other dialog, so the type
   keeps the contrast it was designed with, and the colour still reads as red
   from the far side of the room. */

/* --------------------------------------------------------------------------
   Blurred popup backdrops
   --------------------------------------------------------------------------
   Shared by the renewal prompt and the in-panel checkout so the two popups
   feel like one design rather than two.

   TWO things had to change for the blur to actually be visible, and both are
   load-bearing:

   1. `opacity` MUST NOT animate on the element carrying `backdrop-filter`.
      `.modal-overlay` fades 0 → 1, and an element with opacity < 1 is
      composited into its own group — Chromium drops or mis-renders the
      backdrop filter in that case. So these overlays opt out of the fade
      entirely (`opacity: 1`, no transition) and let the PANEL's existing
      transform animation carry the entrance instead. The popup still
      animates in; the scrim just stops being the thing that animates.

   2. The tint has to be light enough to see through. It was
      rgba(8,8,11,0.66) — two-thirds opaque near-black, which hides the blur
      behind it and reads as a flat solid. At 0.42 the blur is unmistakable
      and the panel still separates cleanly from the page.

   `backdrop-filter` is unsupported on some older browsers; there the tint
   alone still separates the popup, just without softening. */
.renew-modal,
.checkout-modal {
  opacity: 1;
  transition: none;
  background: rgba(8, 8, 11, 0.42);
  backdrop-filter: blur(16px) saturate(115%);
  -webkit-backdrop-filter: blur(16px) saturate(115%);
}

.renew-panel {
  position: relative;
  max-width: 420px;
  padding: 38px 34px 30px;
  text-align: center;
}

/* The lit top edge. A pseudo-element rather than a border so it can be inset
   to the panel's corner radius and fade at both ends — a plain 3px border
   runs the full width and meets the rounded corners as a hard stop. */
.renew-panel::before {
  content: '';
  position: absolute;
  top: 0;
  left: 18px;
  right: 18px;
  height: 2px;
  border-radius: 0 0 2px 2px;
}

.renew-glyph {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 60px;
  height: 60px;
  margin-bottom: 20px;
  border-radius: 50%;
  background: var(--danger-bg-strong);
  color: var(--danger);
  /* A soft halo, so the glyph reads as lit rather than as a flat disc. */
  box-shadow: 0 0 0 8px rgba(251, 113, 133, 0.07);
}

.renew-glyph .icon { width: 27px; height: 27px; }

.renew-title {
  font-family: var(--font-display);
  font-size: 1.34rem;
  font-weight: 600;
  letter-spacing: -0.02em;
  line-height: 1.25;
  color: var(--danger-text);
  margin: 0 0 10px;
}

.renew-lead {
  font-size: 0.9rem;
  line-height: 1.65;
  color: var(--text-secondary);
  /* Capped in characters, not pixels: two short lines read faster than one
     long one, and ch tracks the font rather than guessing at it. */
  max-width: 30ch;
  margin: 0 auto 28px;
}

.renew-lead strong { color: var(--danger-text); font-weight: 600; }

.renew-actions { display: flex; flex-direction: column; gap: 6px; }

@media (max-width: 560px) {
  .renew-panel { padding: 32px 24px 24px; }
  .renew-title { font-size: 1.2rem; }
  .renew-lead { font-size: 0.86rem; margin-bottom: 24px; }
}

/* --------------------------------------------------------------------------
   Settings tabs
   --------------------------------------------------------------------------
   Real links to real URLs, not JavaScript panels — each settings area is
   bookmarkable and works with no script at all. Styled as a tab strip so it
   still reads as one settings screen rather than five separate pages. */

.settings-tabs {
  display: flex;
  gap: 4px;
  margin-bottom: 26px;
  padding-bottom: 2px;
  border-bottom: 1px solid var(--border);
  /* The strip scrolls rather than wrapping: a handful of tabs on a narrow
     window would otherwise stack into two rows and stop reading as a tab bar. */
  overflow-x: auto;
  scrollbar-width: none;
}

.settings-tabs::-webkit-scrollbar { display: none; }

/*
 * Ten tabs, not eight.
 *
 * The success and system logs pushed the strip past the content column: at
 * 1280px it needed 1283px and the last tab — "System log", the new one someone
 * would be looking for — sat off the right edge with the scrollbar hidden, so
 * nothing on screen said it was there. Three pixels of padding per side brings
 * all ten inside a 1280px laptop, which is where this panel is mostly used.
 */
.settings-tabs { gap: 3px; }
.settings-tabs .settings-tab { padding-left: 12px; padding-right: 12px; }

/*
 * Below 1280px it wraps instead of scrolling.
 *
 * The rule above says a scrolling strip reads better than two rows, and that
 * held at five tabs. At ten it stops being true: the scrollbar is hidden by
 * design, so a tab past the edge is a tab nobody can see is there.
 *
 * The breakpoint is measured, not chosen. Ten tabs need 1223px; the content
 * column is 1222px at a 1280px window and 1042px at 1100px. So 1280 is the
 * narrowest width that fits them all on one line, and every width below it
 * wraps rather than hiding the last one.
 */
@media (max-width: 1279px) {
  .settings-tabs {
    flex-wrap: wrap;
    overflow-x: visible;
    row-gap: 2px;
  }
}

.settings-tab {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  flex: none;
  padding: 10px 15px;
  border-radius: 9px 9px 0 0;
  font-size: 0.85rem;
  font-weight: 500;
  color: var(--text-tertiary);
  text-decoration: none;
  white-space: nowrap;
  /* The active underline sits ON the container's border, so it needs to
     overhang by exactly that 1px to cover it rather than sit above it. */
  position: relative;
  bottom: -1px;
  border-bottom: 2px solid transparent;
  transition: color 120ms var(--ease), background-color 120ms var(--ease);
}

.settings-tab .icon { width: 15px; height: 15px; }

.settings-tab:hover {
  color: var(--text-primary);
  background: var(--bg-hover);
}

.settings-tab.is-active {
  color: var(--accent-text);
  border-bottom-color: var(--accent);
}

.settings-tab.is-active .icon { color: var(--accent-text); }

@media (max-width: 640px) {
  .settings-tabs { margin-bottom: 20px; }
  .settings-tab { padding: 9px 12px; font-size: 0.8rem; }
  /* Labels alone are enough on a phone; the icons are what push the strip
     wide enough to need scrolling in the first place. */
  .settings-tab .icon { display: none; }
}

/* === Touch targets =======================================================
   A fingertip covers far more than a mouse cursor: both platform guidelines
   put the floor at 44 CSS px (iOS) / 48dp (Android). Several controls here
   are sized for a pointer — 32px small buttons, a 30px field affix, a 38px
   menu toggle — which is comfortable on a desktop and a miss-prone target on
   a phone.

   Rather than enlarge them everywhere and coarsen the desktop layout, they
   are grown only where the pointer is actually a finger. `pointer: coarse`
   is the honest test; the width query is the fallback, because a phone
   browser in desktop-site mode reports a fine pointer while still being
   operated by a thumb.

   Only the box grows. Type size, colour and spacing are untouched, so
   nothing about the design reads differently — the controls simply become
   hittable. */

/* The header's own coarse-pointer sizes (.mobile-toggle, .header-btn, .logo)
   live in header.css. That file loads after this one, so a rule for them here
   would lose to its base sizes and the targets would quietly stay small. */

@media (pointer: coarse), (max-width: 768px) {

  /* Row actions — Approve, Reject, Pay, Edit — are the small buttons, and on
     a phone they are the primary actions on the page, not secondary ones. */
  .btn-sm { --btn-h: 40px; padding: 0 14px; }

  /* Clear/reveal buttons sitting inside a field. The field itself is at least
     40px tall, so a 36px affix fits inside it without changing its height. */
  .input-affix-btn { width: 36px; height: 36px; }

  /* Status filter chips: a row of adjacent targets, where being short makes
     it easy to hit the neighbour instead. */
  .filter-btn {
    display: inline-flex;
    align-items: center;
    min-height: 40px;
  }

  /* The title in a table row is how that row is opened, and as a bare line of
     text its hit box is only as tall as the text — around 17px. The padding
     grows the box and the matching negative margin takes the space back, so
     the target becomes finger-sized without the row changing height. */
  .table-link {
    display: inline-block;
    padding-block: 12px;
    margin-block: -12px;
  }
}

/* === Small-type floor on phones ==========================================
   The micro-labels are set around 0.7rem, which is fine on a desktop monitor
   at arm's length and thin on a handset held further away in poor light.
   Only the smallest steps are lifted, and only on a phone, so the type scale
   keeps its proportions everywhere else. */

@media (max-width: 640px) {
  .page-eyebrow { font-size: 0.75rem; }
  .data-table th { font-size: 0.75rem; }
  .status-badge { font-size: 0.75rem; }
}

/* === Password fields with a reveal control ===============================
   partials/secret-field.ejs, used by BOTH panels: the admin's stored secrets
   (SFTP, Drive, SMTP) and the change-password form on the artist profile and
   the admin account screen. It lives here rather than in admin.css for that
   reason — the artist panel never downloads admin.css, and the eye rendered
   there as an unstyled button sitting below its own input.

   The button sits inside the field's box rather than beside it, so the input
   keeps the full width of its form-row column and the row still lines up with
   a plain field opposite it. */

.secret-field { position: relative; }

/* Room for the button, so a long value slides under it rather than behind it. */
.secret-field .form-input { padding-right: 42px; }

.secret-reveal {
  position: absolute;
  top: 50%;
  right: 6px;
  transform: translateY(-50%);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 30px;
  height: 30px;
  padding: 0;
  border: 0;
  border-radius: 8px;
  background: transparent;
  color: var(--text-tertiary);
  cursor: pointer;
  transition: color 0.15s ease, background 0.15s ease;
}

.secret-reveal:hover { color: var(--text-primary); background: var(--bg-hover); }
.secret-reveal:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
.secret-reveal .icon { width: 17px; height: 17px; }

/* One glyph at a time: the open eye while hidden, the struck-through eye while
   showing — the icon says what pressing it will undo. */
.secret-eye-shut { display: none; }
.secret-reveal[aria-pressed='true'] .secret-eye-open { display: none; }
.secret-reveal[aria-pressed='true'] .secret-eye-shut { display: inline-block; }

/* === Change password =====================================================
   partials/password-change.ejs. The two new-password boxes sit side by side in
   a .form-row on a wide screen and stack on a narrow one, which the shared
   .form-row already does; only the sign-off note needs anything of its own. */

/* A rule under the heading, so the form below reads as its own thing rather
   than as more of whatever card it was dropped into. The layout itself comes
   from .card-head, which this is always paired with. */
.password-change-head {
  padding-bottom: 16px;
  margin-bottom: 20px;
  border-bottom: 1px solid var(--border-subtle);
}

/* .form-card-title carries its own border-bottom, for the case where it is a
   lone heading at the top of a card. Here the heading sits beside an icon
   inside a flex row, so that border draws a short rule starting after the icon
   and stopping at the text — on top of the full-width one above. One rule, on
   the container, is the intended look. */
.password-change-head .form-card-title {
  margin: 0 0 4px;
  padding-bottom: 0;
  border-bottom: 0;
}

.password-change-head .form-hint { margin: 0; max-width: 62ch; }

.password-change-note {
  margin: 4px 0 18px;
  max-width: 62ch;
}
