/* nine71 Platform — same tokens as Foundry and Signal.
 *
 * A deliberate copy of the shared surface tokens rather than an import: the three
 * surfaces are separate static bundles with no build step, so there is nothing to
 * resolve an @import through, and a cross-directory relative link breaks the
 * moment one of them moves. The data-series colours are NOT redefined here —
 * this surface has no charts.
 */
:root {
  --bg: #0b0b0d;
  --surface: #131316;
  --surface-2: #1a1a1f;
  --border: #232329;
  --border-2: #3a3a44;                    /* hover borders, focus, dashed wells */
  --border-soft: rgba(35, 35, 41, 0.7);   /* table row rules */
  --text: #f2f2f4;
  --text-2: #c6c6cf;                      /* body-adjacent: artist column, filter rows */
  --text-3: #7f7f8a;                      /* quieter than muted: unknown buckets, passed */
  --muted: #8e8e98;
  --dim: #55555e;                         /* furniture: the footer */
  --lilac: #cdbcf2;
  --cream: #efe7cf;
  --good: #7fc47f;
  --danger: #d96a6a;
  /* The pair every "you are here" fill is built from: white pill on the dark
     theme, near-black pill on the light one. Naming the PAIR rather than the
     colours is what lets one attribute flip every active control at once. */
  --invert-bg: #fff;
  --invert-fg: #0b0b0d;
  --row-open: rgba(26, 26, 31, 0.55);
  --rail-bg: #0d0d10;
  --rail: 250px;
  /* 10px, down from 18. The deep radius read as a consumer app; this surface is
     a working console and its corners should say so. One token, so every card,
     table well and panel moves together. */
  --r: 10px;
  /* Controls and small parts sit one step tighter than the cards they live on. */
  --r-ctl: 8px;
  --r-chip: 6px;
  --font: 'Outfit', -apple-system, 'BlinkMacSystemFont', 'Segoe UI', 'Roboto', sans-serif;

  /* Native widgets (selects, scrollbars) follow the surface, not the OS. */
  color-scheme: dark;
}

/* ---------- light ----------
 * One attribute on <html>, set before first paint by the inline snippet in
 * index.html and flipped by js/theme.js. Only tokens change here — every rule
 * below this block draws from them, so a colour that needs a light value it
 * does not get is a bug in the token list, not a case for a scoped override.
 *
 * The two accents darken rather than survive: lilac #cdbcf2 and cream #efe7cf
 * are highlight colours on near-black and would be invisible ink on white.
 * Same hue, moved to where it carries on this ground. --good and --danger
 * likewise. The data-series pair is untouched — this surface has no charts. */
:root[data-theme="light"] {
  --bg: #ffffff;              /* the whole page is white (Nikhil, 2026-09-09) */
  --surface: #ffffff;
  --surface-2: #ebebef;
  --border: #d8d8e0;
  --border-2: #b9b9c6;
  --border-soft: rgba(211, 211, 221, 0.8);
  --text: #17171c;
  --text-2: #3d3d49;
  --text-3: #686873;
  --muted: #5c5c68;
  --dim: #6d6d78;
  --lilac: #6f56bd;
  --cream: #8a6d1f;
  --good: #2f7e2f;
  --danger: #b04545;
  --invert-bg: #17171a;
  --invert-fg: #ffffff;
  --row-open: rgba(232, 232, 238, 0.7);
  --rail-bg: #ececf0;

  color-scheme: light;
}

/* The wordmark ships as a white SVG (licensing: outlines, not a webfont — see
   the note at .wordmark-img). Pure white inverts to pure black exactly, so the
   light theme gets the black mark without a second asset to keep in step. */
:root[data-theme="light"] .wordmark-img { filter: invert(1); }

* { box-sizing: border-box; margin: 0; padding: 0; }
html, body { height: 100%; }
body {
  background: var(--bg);
  color: var(--text);
  font-family: var(--font);
  font-size: 15px;
  -webkit-font-smoothing: antialiased;
  display: flex; flex-direction: column;
}
#shell { display: flex; flex-direction: column; min-height: 100vh; }
.hidden { display: none !important; }
.is-error { color: var(--danger); }

/* ---------- gate ---------- */
.gate {
  position: fixed; inset: 0; background: var(--bg); z-index: 50;
  display: flex; align-items: center; justify-content: center; padding: 20px;
}
.gate-card {
  background: var(--surface); border: 1px solid var(--border); border-radius: var(--r);
  padding: 38px 40px; width: 100%; max-width: 380px;
}
.gate-card .wordmark { margin-bottom: 10px; }
.gate-card form { display: flex; flex-direction: column; gap: 9px; margin-top: 20px; }
.gate-error { color: var(--danger); font-size: 13px; margin-top: 12px; min-height: 16px; }
.gate-alt { margin-top: 6px; }
.gate-alt a { color: var(--muted); text-decoration: underline; text-underline-offset: 2px; }
.gate-alt a:hover { color: var(--text); }

/* The account sign-in (2026-08-18). The card grew from one field to a provider
   button, a form, three alternates and a destination picker, so the pieces get
   named rather than inheriting spacing from `.gate-card form` alone. */
.gate-providers { display: flex; flex-direction: column; gap: 9px; margin-top: 20px; }
/* The second of the gate's two buttons ("Sign in and get started", 2026-08-28).
   An anchor wearing .btn: block and centred so it reads as the form's sibling,
   not as a link that wandered in. */
.gate-start {
  display: block; width: 100%; text-align: center; margin-top: 9px;
  text-decoration: none; box-sizing: border-box;
}
.btn.provider {
  display: flex; align-items: center; justify-content: center; gap: 9px;
  width: 100%; padding: 11px 16px;
}
/* A rule with the word sitting in the gap. The pseudo-elements are the rule, so
   the label needs no background matching the card behind it — one less colour to
   keep in step across the two themes. */
.gate-or { display: flex; align-items: center; gap: 10px; margin: 16px 0 4px; }
.gate-or::before, .gate-or::after {
  content: ''; flex: 1; height: 1px; background: var(--border);
}
.gate-or span { color: var(--dim); font-size: 11.5px; letter-spacing: 0.04em; text-transform: uppercase; }
/* Hidden together with the form it separates, when the project offers no
   provider at all — a rule reading "or" above nothing is furniture. */
.gate-providers[hidden] + .gate-or { display: none; }

/* A button that reads as a link. The platform had no such control until the gate
   needed three of them; Foundry's is the same shape, and copying it keeps the two
   sign-ins recognisable as one product. */
.linkish {
  background: none; border: 0; padding: 0; font: inherit; cursor: pointer;
  color: var(--text-2); text-decoration: underline; text-underline-offset: 2px;
}
.linkish:hover { color: var(--text); }
.linkish:disabled { color: var(--dim); cursor: default; text-decoration: none; }

/* The destination picker. Each row is a whole button: the name is the target and
   the second line says what kind of thing it is, so there is nothing on the row
   that is not part of the choice. */
.gate-dests { display: flex; flex-direction: column; gap: 8px; margin-top: 18px; }
.gate-dest {
  display: flex; flex-direction: column; align-items: flex-start; gap: 2px;
  width: 100%; text-align: left; padding: 12px 14px;
  background: var(--surface-2); border: 1px solid var(--border);
  border-radius: var(--r-ctl); color: var(--text); font-family: inherit; cursor: pointer;
}
.gate-dest:hover:not(:disabled) { border-color: var(--border-2); }
/* An anchor wearing the row (the Foundry row for a personal account). */
a.gate-dest { text-decoration: none; }
.gate-dest:disabled { cursor: default; opacity: 0.55; }
.gate-dest-name { font-size: 14px; font-weight: 600; }
.gate-dest-sub { font-size: 12px; color: var(--muted); }
/* ---------- the rail ----------
   Foundry's shell, spoken here (2026-08-13): identity and the theme toggle at
   the top, destinations that explain themselves, the session at the foot. The
   buttons keep class="pill" because app.js wires and toggles them by that
   class; only their clothes changed. The current item is a rule and a weight,
   not a filled capsule - the filled pill was the brightest object on a matte
   page, and it put the emphasis on the navigation instead of the work. */
.home-link { text-decoration: none; color: inherit; }
.wordmark { font-size: 19px; display: flex; align-items: baseline; }
.module-label {
  color: var(--muted); font-size: 12px; font-weight: 500;
  letter-spacing: 0; margin-left: 10px; align-self: baseline;
}

/* The rail only sticks if #shell is taller than it is - the same trap foundry
   measured: html,body at height 100% caps the body at the viewport, the
   document scrolls, and sticky has no room to travel. The gate is
   position:fixed and does not care. */
body { height: auto; min-height: 100%; }
#shell { flex-direction: row; align-items: stretch; }
/* THE RAIL (2026-09-09, Nikhil): a gray column, the work area white, one
   hairline between them. The nav is grouped — an all-caps describer, plain
   items under it, no second lines — and the current item is a white tile with
   a hairline border and full-black text. The rail gray and that black are the
   two hand-set colours on this surface; both are light-theme values, which is
   the default here, and the dark theme keeps token equivalents. */
:root[data-theme="light"] .side {
  --rail-bg: #f3f3f6;
  --rail-hover: #e8e8ee;
  --rail-current-fg: #000000;
}
.side {
  --rail-hover: var(--surface-2);
  --rail-current-fg: var(--text);
  flex: 0 0 var(--rail); width: var(--rail);
  display: flex; flex-direction: column;
  border-right: 1px solid var(--border);
  background: var(--rail-bg);
  padding: 18px 12px 14px;
  position: sticky; top: 0; height: 100vh; overflow-y: auto;
}
.side-top {
  display: flex; align-items: center; justify-content: space-between; gap: 10px;
  padding: 2px 6px 18px;
}
.side-nav { display: flex; flex-direction: column; gap: 0; }
.rail-group { display: flex; flex-direction: column; gap: 2px; padding-bottom: 16px; }
.rail-group:last-child { padding-bottom: 0; }
/* A group whose every pill app.js has hidden (the org pair, outside an org
   session) hides its describer with them rather than heading nothing. */
.rail-group:not(:has(.pill:not(.hidden))) { display: none; }
.rail-sec {
  font-size: 11px; font-weight: 500; text-transform: uppercase;
  letter-spacing: 0.08em; color: var(--muted);
  padding: 0 10px 6px; margin: 0;
}
/* One line, 36px, 8px corners. The border is drawn transparent at rest so a
   current item does not shift by a hairline when it gains one. */
.pill {
  display: flex; align-items: center;
  text-align: left; width: 100%;
  height: 36px; min-height: 36px; padding: 0 10px;
  border: 1px solid transparent; border-radius: 8px;
  background: none; cursor: pointer;
  font-family: inherit; color: var(--text-2);
}
.pill:hover { background: var(--rail-hover); color: var(--text); }
.pill.active { background: var(--surface); border-color: var(--border); color: var(--rail-current-fg); }
.pill:focus-visible { outline: 2px solid var(--lilac); outline-offset: 2px; }
.nav-name { font-size: 14px; font-weight: 500; line-height: 1; }
/* No second lines in the rail (2026-09-09). The class stays defined so a
   stray .nav-sub in cached markup renders as nothing rather than as text. */
.nav-sub { display: none; }

/* position: relative anchors the account menu, which opens upward from the
   chip the way a switcher pinned to a bottom-left corner does. */
.side-foot {
  margin-top: auto; padding-top: 10px; border-top: 1px solid var(--border);
  display: flex; flex-direction: column; gap: 8px; align-items: stretch;
  position: relative;
}

/* ---------- the account chip + menu ----------
   FOUNDRY'S CONTROL, WORN HERE VERBATIM (2026-08-27): same class names, same
   shapes, same manners, so the bottom-left of either surface is the one place
   your account lives. Only the accent differs — the avatar wears this
   surface's lilac where Foundry's wears its amber. */
.account {
  display: flex; align-items: center; gap: 11px; width: 100%;
  border: 1px solid transparent; background: none; cursor: pointer;
  font-family: inherit; text-align: left;
  padding: 9px 10px; border-radius: 12px;
}
.account:hover { background: var(--surface); border-color: var(--border); }
.account:focus-visible { outline: 2px solid var(--lilac); outline-offset: 2px; }
.account-lines { display: flex; flex-direction: column; gap: 1px; min-width: 0; }
.account-who {
  font-size: 13px; color: var(--text); font-weight: 500;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap; max-width: 155px;
}
.account-act { font-size: 11.5px; color: var(--muted); }
.avatar {
  flex: none; width: 30px; height: 30px; border-radius: 50%;
  display: grid; place-items: center;
  font-size: 12.5px; font-weight: 600; letter-spacing: 0.02em;
  border: 1px solid var(--lilac); color: var(--invert-fg); background: var(--lilac);
}
.account-menu {
  position: absolute;
  bottom: calc(100% + 8px);
  left: 0;
  min-width: 230px;
  max-width: 290px;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: 14px;
  padding: 8px;
  box-shadow: 0 12px 40px rgba(0, 0, 0, 0.5);
  z-index: 40;
}
.am-ident {
  color: var(--muted); font-size: 11.5px; padding: 4px 10px 8px; margin: 0;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.am-row {
  display: flex; flex-direction: column; align-items: flex-start; gap: 1px;
  width: 100%; padding: 8px 10px; border: none; border-radius: 9px;
  background: none; color: var(--text); font: inherit; text-align: left;
  cursor: pointer;
}
.am-row:hover:not(:disabled) { background: var(--surface-2); }
.am-row:disabled { cursor: default; opacity: 0.55; }
.am-row .am-name { font-weight: 600; font-size: 13.5px; }
.am-row .am-sub { font-size: 11px; color: var(--muted); }
.am-row .am-name,
.am-row .am-sub { max-width: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.am-rule { border-top: 1px solid var(--border); margin: 6px 4px; }
.am-item {
  display: block; width: 100%; padding: 7px 10px; border: none; border-radius: 9px;
  background: none; color: var(--text); font: inherit; font-size: 13px;
  text-align: left; cursor: pointer;
}
.am-item:hover { background: var(--surface-2); }

/* The org workspace switcher: rail furniture, sitting where the label nav is
   about to begin, styled like a pill's quiet parts rather than a form. */
.org-switch {
  display: flex; flex-direction: column; gap: 6px;
  padding: 0 12px 16px 15px;
}
.org-switch-label {
  font-size: 11.5px; color: var(--text-3); line-height: 1.35;
}
.org-switch .set-select { width: 100%; font-size: 13px; }
.work { flex: 1; min-width: 0; display: flex; flex-direction: column; }

/* ---------- the theme toggle ----------
   Quiet by design: it is furniture, not a feature, and the active-pill white
   would make it the brightest thing in the corner. The icon shows what pressing
   it GETS you — a sun while dark, a moon while light — swapped on the same
   attribute that swaps the tokens, so button and page cannot disagree. */
.theme-toggle {
  display: inline-flex; align-items: center; justify-content: center;
  width: 30px; height: 30px; flex: none;
  border: 1px solid var(--border); border-radius: 50%;
  background: none; color: var(--muted); cursor: pointer;
}
.theme-toggle:hover { color: var(--text); border-color: var(--border-2); }
.theme-toggle:focus-visible { outline: 2px solid var(--lilac); outline-offset: 2px; }
.theme-toggle .tt-moon { display: none; }
:root[data-theme="light"] .theme-toggle .tt-sun { display: none; }
:root[data-theme="light"] .theme-toggle .tt-moon { display: block; }
/* The gate has no header to sit in, so the same control pins to the same spot. */
.theme-toggle-fixed { position: fixed; top: 18px; left: 18px; z-index: 60; }
/* .account-state the CLASS is gone — the id survives as the chip's first
   line (.account-who), so every existing write of the workspace name lands
   on the new control unchanged. */

/* ---------- layout ----------
   Fluid, not a fixed well. The old 1080px cap centred a narrow column on any
   real monitor and read as empty margin, not as design (Nikhil, 2026-08-26:
   fitment should follow the screen). The gutters now scale with the viewport
   and the cap only exists so an ultrawide does not stretch table rows past
   legibility. */
main {
  flex: 1; width: 100%; max-width: 1560px; margin: 0 auto;
  padding: clamp(12px, 1.6vw, 24px) clamp(14px, 2.6vw, 40px) 48px;
}
.view { display: none; }
.view.active { display: flex; flex-direction: column; gap: 14px; }
.card {
  /* No outline (Nikhil, 2026-08-21: "I like the look without the thin grey
     outlines"). The surface tone against the page ground is the card's whole
     edge, in both themes. */
  background: var(--surface);
  border-radius: var(--r); padding: 16px 20px;
}
/* The head is a toolbar, not a heading floating in space: the rule under it is
   what makes a card read as a titled region of a console rather than a blurb. */
.card-head {
  display: flex; align-items: baseline; justify-content: space-between;
  gap: 12px; margin-bottom: 14px;
  padding-bottom: 10px; border-bottom: 1px solid var(--border-soft);
}
h2 { font-size: 15.5px; font-weight: 600; letter-spacing: -0.2px; }
.small-cap { font-size: 12px; }
.muted { color: var(--muted); font-size: 13.5px; line-height: 1.55; }
.upload-note { margin-top: 12px; }
.search-hint { font-size: 12px; margin: 8px 0 14px; }
footer { text-align: center; color: var(--dim); font-size: 12px; padding: 16px; }

/* ---------- controls ---------- */
/* EVERY TEXTUAL INPUT, and `number` is one of them. It was missing from this
   list, which is why the two boxes on the upload-codes row rendered as raw
   browser chrome -- a white field with a system border -- sitting beside the
   styled note field in the same form. Nothing had gone wrong in that form; the
   selector simply never named the type. `tel` and `url` are here for the same
   reason, before the next one is added and lands outside the design again. */
input[type=text], input[type=password], input[type=email], input[type=search],
input[type=number], input[type=tel], input[type=url] {
  background: var(--surface-2); border: 1px solid var(--border); color: var(--text);
  border-radius: var(--r-ctl); padding: 9px 12px; font-size: 14px; font-family: inherit;
  outline: none; width: 100%;
}

/* The spinner is a browser's own widget and cannot be themed, so it is the one
   piece of OS chrome that survives the rule above. These fields are bounded by
   min/max and typed into, not nudged one at a time. */
input[type=number] { -moz-appearance: textfield; appearance: textfield; }
input[type=number]::-webkit-outer-spin-button,
input[type=number]::-webkit-inner-spin-button { -webkit-appearance: none; margin: 0; }

/* A NUMBER WITH ITS UNIT ATTACHED. Two bare boxes reading "90" and "200" told
   the reader nothing -- the meaning was in `title` and `aria-label`, which are
   invisible until you hover or run a screen reader. The unit sits inside the
   field's own border so it reads as one control. */
.num-field { position: relative; display: inline-flex; align-items: center; }
.num-field input[type=number] { width: 100%; padding-right: 4.6em; }
.num-field .num-unit {
  position: absolute; right: 12px; pointer-events: none;
  color: var(--muted); font-size: 12.5px; white-space: nowrap;
}
input:focus, .set-select:focus { border-color: var(--border-2); }
/* The select keeps the field styling but loses the OS dropdown arrow, which is
   the single most dated thing on these screens: a platform-native triangle in
   the middle of a designed form, rendering differently on every machine. The
   replacement is an inline SVG chevron in the background -- same shape the rest
   of the product uses -- with room reserved for it on the right. */
.set-select {
  background: var(--surface-2); border: 1px solid var(--border); color: var(--text);
  border-radius: var(--r-ctl); padding: 9px 12px; font-size: 14px; font-family: inherit;
  outline: none; width: 100%;
  -webkit-appearance: none; -moz-appearance: none; appearance: none;
  padding-right: 32px;
  background-image: url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12' fill='none'%3E%3Cpath d='M2.5 4.5L6 8L9.5 4.5' stroke='%23888' stroke-width='1.6' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
  background-repeat: no-repeat;
  background-position: right 11px center;
}
/* `appearance: none` also drops the OS focus ring on a select, so the focus
   state has to be stated or the control becomes unreachable-looking by
   keyboard. Matches input:focus above. */
.set-select:focus-visible { border-color: var(--border-2); }
.btn {
  font-family: inherit; font-size: 13.5px; font-weight: 500; cursor: pointer;
  border-radius: var(--r-ctl); padding: 9px 15px; border: 1px solid var(--border);
  background: var(--surface-2); color: var(--text); white-space: nowrap;
}
.btn.primary { background: var(--invert-bg); color: var(--invert-fg); border-color: var(--invert-bg); }
.btn.ghost { background: none; color: var(--muted); }
.btn.ghost:hover:not(:disabled) { color: var(--text); border-color: var(--border-2); }
.btn:disabled { opacity: 0.45; cursor: default; }
.btn.small { padding: 7px 13px; font-size: 12.5px; }
.inline-form { display: flex; gap: 8px; margin-top: 10px; }
.label-row { display: flex; gap: 8px; align-items: center; }

/* ---------- dropzone ---------- */
.dropzone {
  border: 1.5px dashed var(--border-2); border-radius: var(--r); padding: 30px 18px;
  text-align: center; cursor: pointer; display: flex; flex-direction: column; gap: 5px;
}
.dropzone:hover, .dropzone.drag { border-color: var(--lilac); background: var(--surface-2); }
.dropzone:focus-visible { outline: 2px solid var(--lilac); outline-offset: 2px; }
.dz-big { font-size: 15px; font-weight: 600; }
.dz-small { color: var(--muted); font-size: 12.5px; }

/* ---------- ingest queue ---------- */
.queue-list { display: flex; flex-direction: column; }
.queue-row {
  display: grid; grid-template-columns: 1fr 130px 150px;
  gap: 14px; align-items: center;
  padding: 12px 2px; border-top: 1px solid var(--border);
}
.queue-row:first-child { border-top: none; }
.q-main { min-width: 0; display: flex; flex-direction: column; gap: 2px; }
.q-name {
  font-size: 14px; font-weight: 500;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.q-label { font-size: 12px; color: var(--muted); }
.q-detail { font-size: 12px; color: var(--cream); }
.q-bar { background: var(--surface-2); border-radius: 999px; height: 8px; overflow: hidden; }
.q-bar div { background: var(--lilac); height: 100%; border-radius: 999px; transition: width 160ms linear; }
.q-status { font-size: 12.5px; color: var(--muted); text-align: right; }
.q-status.good { color: var(--good); }
.q-status.bad { color: var(--danger); }
@media (max-width: 720px) {
  .queue-row { grid-template-columns: 1fr 90px; }
  .q-status { grid-column: 1 / -1; text-align: left; }
}

/* ---------- tracklist ---------- */
.filters { display: grid; grid-template-columns: 1fr 190px 190px; gap: 8px; }
@media (max-width: 780px) { .filters { grid-template-columns: 1fr; } }

/* The table scrolls inside its own box; the page must never scroll sideways. */
.table-scroll { overflow-x: auto; }
.track-table { width: 100%; border-collapse: collapse; font-size: 13.5px; }
.track-table th {
  text-align: left; font-size: 11px; text-transform: uppercase; letter-spacing: 0.7px;
  color: var(--muted); font-weight: 600; padding: 8px 10px 8px 0;
  border-bottom: 1px solid var(--border); white-space: nowrap;
  position: sticky; top: 0; background: var(--surface);
}
.track-table td {
  padding: 9px 10px 9px 0; border-bottom: 1px solid var(--border-soft);
  vertical-align: middle;
}
.track-table tr:hover td { background: var(--surface-2); }
.track-table .num { text-align: right; font-variant-numeric: tabular-nums; }
.col-title { font-weight: 500; max-width: 320px; }
.col-artist { color: var(--text-2); max-width: 200px; }
.col-label, .col-added { color: var(--muted); white-space: nowrap; }
.col-title, .col-artist, .col-label { overflow: hidden; text-overflow: ellipsis; }
.col-flags { white-space: nowrap; text-align: right; }

/* ---------- catalogue covers (migration 026) ----------
   A fixed square, so a row's height is decided once whether its cover has
   arrived, is still loading, or does not exist. The placeholder carries a
   quiet glyph rather than emptiness — an empty white box already failed
   review once on the producer side. */
.col-art { width: 52px; }
.cat-art {
  display: block; width: 44px; height: 44px; object-fit: cover;
  border-radius: 8px; border: 1px solid var(--border);
}
.cat-art-ph {
  display: grid; place-items: center; width: 44px; height: 44px;
  border-radius: 8px; background: var(--surface-2); border: 1px solid var(--border);
  color: var(--text-3); font-size: 14px;
}
.cat-art-ph::after { content: '\266A'; }

/* Rectangles, not capsules. The 999px pill is the one shape this restyle
   retires everywhere a value is being labelled: a tag on a console is a small
   squared token, and the capsule count was what made the screens read as a
   consumer app. Progress bars keep their round ends — they are meters, not
   labels. */
.chip {
  background: var(--surface-2); border: 1px solid var(--border); color: var(--text-2);
  font-size: 12px; padding: 3px 9px; border-radius: var(--r-chip); white-space: nowrap;
}
.chip.tiny { font-size: 10.5px; padding: 2px 8px; margin-left: 4px; }
.chip.good { color: var(--good); border-color: rgba(127, 196, 127, 0.35); }

/* ---------- your sound, in plain words ----------
   The sentence reads at body-plus scale — it is the one thing on the screen a
   label owner repeats out loud, so it earns the size. The chips reuse .chip;
   dashed means "a measurement still running", the same different-KIND-of-answer
   language the inbox filters use for their unknown buckets. */
.sound-words { font-size: 17px; line-height: 1.5; letter-spacing: -0.1px; margin-bottom: 4px; }
.sw-chips { margin-top: 12px; margin-bottom: 10px; }
.chip.sw-unmeasured { border-style: dashed; color: var(--text-3); background: none; }

.pager {
  display: flex; align-items: center; justify-content: space-between;
  gap: 12px; margin-top: 14px;
}
.pager .muted { font-size: 12.5px; }

/* Per-upload options */
.check { display: flex; gap: 10px; align-items: flex-start; margin-bottom: 14px; }
.check input { margin-top: 3px; flex: 0 0 auto; accent-color: var(--lilac); }
.check span { font-size: 13.5px; }
.check em {
  display: block; color: var(--muted); font-size: 12.5px;
  font-style: normal; line-height: 1.5; margin-top: 3px;
}
.check strong { color: var(--cream); font-weight: 600; }

/* The wordmark is an SVG, not text. Coolvetica's licence permits "logo" and
   "web page (not embedded)" but forbids "web page (embedded)", so it cannot ship
   as an @font-face webfont — outlines are the licensed route. Height in em so the
   existing font-size rules still control its scale; 0.78 matches the optical size
   the live text had, since a glyph's ink is shorter than its em box. */
.wordmark-img { display: block; height: 0.78em; width: auto; }

/* ===========================================================================
 * The two dashboards — label workspace (Inbox / Your sound / Catalogue) and
 * owner overview (Overview / Access).
 *
 * Same tokens, same 18px radius, same one-card-per-idea layout as the two
 * screens above. Nothing new is introduced: the stat blocks, the meters and the
 * inbox rows are built from --surface-2, --border and --lilac, so these screens
 * read as more of this product rather than as a second one bolted on.
 *
 * Two colours carry meaning and are used nowhere else: --danger for a fired
 * alarm and for a value that contradicts another stored value, --good for a
 * record that counts toward a profile. A muted grey is the third state and it
 * means "not measured" — never zero, never fine.
 * ======================================================================== */

/* Outfit has tabular figures; without this a column of scores wobbles as the
   digits change, which reads as sloppiness on the one screen that is entirely
   numbers. */
.mono { font-variant-numeric: tabular-nums; }
.tiny { font-size: 12px; line-height: 1.5; }
.sec-head { margin: 14px 0 6px; text-transform: uppercase; letter-spacing: 0.7px; font-size: 11px; }
.footnote { margin: 2px 4px 0; }

/* #overview-body and #sound-body hold several cards each. */
#overview-body, #sound-body { display: flex; flex-direction: column; gap: 14px; }
.bare-head { padding: 12px 20px; }
.bare-head .card-head {
  margin-bottom: 0; align-items: center;
  padding-bottom: 0; border-bottom: none;
}

/* ---------- numbers ---------- */
.stat-row {
  display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
  gap: 10px; margin: 4px 0 14px;
}
.stat-row.tight { margin: 14px 0 18px; }
/* KPI tiles the way every ops console draws them: the label ABOVE the figure,
   small and even, so a row of four scans as a header row over a number row.
   column-reverse because the markup order (number, label) predates this and is
   shared with the owner screens. */
.stat {
  background: var(--surface-2); border: 1px solid var(--border); border-radius: var(--r-ctl);
  padding: 10px 14px 12px; display: flex; flex-direction: column-reverse; gap: 4px; min-width: 0;
}
.stat-n { font-size: 22px; font-weight: 600; letter-spacing: -0.4px; font-variant-numeric: tabular-nums; }
.stat-k {
  color: var(--muted); font-size: 10.5px; line-height: 1.35;
  text-transform: uppercase; letter-spacing: 0.06em; font-weight: 500;
}

.kv { display: grid; grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); gap: 2px 18px; margin-top: 12px; }
.kv > div, .ib-facts > div {
  display: flex; justify-content: space-between; gap: 12px;
  padding: 7px 0; border-top: 1px solid var(--border-soft); min-width: 0;
}
.kv .k, .ib-facts .k { color: var(--muted); font-size: 12.5px; white-space: nowrap; }
.kv .v, .ib-facts .v { font-size: 12.5px; text-align: right; min-width: 0; }
/* Wraps rather than truncates. The values here include a fingerprint description
   and the result of the filename check, and an ellipsis on either of those hides
   the half that carries the meaning — "passed — a rename does not move a s…". */
.kv .v { white-space: normal; overflow-wrap: anywhere; }
/* The SAME fix the .kv .v rule above documents, which this selector never got.
   What lands here is a paragraph: the released-record match, and after it the
   producer's dispute of that match in their own words — the one statement on
   this panel anybody is legally interested in, and the part an ellipsis ate
   first. Left-aligned because a wrapped sentence set right is worse than a
   clipped one. */
.ib-facts .v { white-space: normal; overflow-wrap: anywhere; text-align: left; }

/* ---------- alarms ---------- */
.alarm {
  color: var(--danger); font-size: 13.5px; font-weight: 600;
  display: flex; gap: 8px; align-items: baseline; margin-top: 2px;
}
.alarm::before { content: "▲"; font-size: 10px; }
.alarm-block { padding: 12px 0; border-top: 1px solid var(--border); }
.alarm-block:first-child { border-top: none; padding-top: 0; }
.alarm-list { list-style: none; margin: 8px 0 0; display: flex; flex-direction: column; gap: 5px; }
.alarm-list li { font-size: 13px; padding-left: 14px; position: relative; }
.alarm-list li::before { content: "·"; position: absolute; left: 4px; color: var(--muted); }
.alarm-list .muted { font-size: 12px; }

/* ---------- meters and the spread band ---------- */
.meter {
  display: block; height: 8px; border-radius: 999px; background: var(--surface-2);
  border: 1px solid var(--border); overflow: hidden;
}
.meter > span { display: block; height: 100%; background: var(--lilac); }

.band { margin: 16px 0 4px; }
.band-track {
  position: relative; height: 22px; border-radius: 999px;
  background: var(--surface-2); border: 1px solid var(--border);
}
.band-sd { position: absolute; top: 0; bottom: 0; background: rgba(205, 188, 242, 0.22); }
.band-mean { position: absolute; top: -3px; bottom: -3px; width: 2px; background: var(--lilac); }
.band-axis {
  display: flex; justify-content: space-between; color: var(--muted);
  font-size: 11.5px; margin-top: 6px;
}
.band-axis span:nth-child(2) { color: var(--cream); }

/* ---------- inbox ---------- */
.filters-row {
  display: flex; align-items: center; justify-content: space-between;
  gap: 12px; flex-wrap: wrap; margin: 18px 0 14px;
}
.chips { display: flex; gap: 6px; flex-wrap: wrap; }
.filters-right { display: flex; gap: 8px; align-items: center; }
.filters-right .set-select { width: auto; padding: 7px 10px; font-size: 12.5px; }
.chip-btn {
  font-family: inherit; font-size: 12.5px; cursor: pointer; white-space: nowrap;
  background: var(--surface-2); border: 1px solid var(--border); color: var(--muted);
  border-radius: var(--r-chip); padding: 6px 12px;
}
.chip-btn.active { background: var(--invert-bg); color: var(--invert-fg); border-color: var(--invert-bg); }
.chip-n { opacity: 0.6; margin-left: 3px; font-variant-numeric: tabular-nums; }

/* ---------- the inbox sidebar ----------
   Filters live in a left rail, list on the right — the layout every pro tool
   trains people on. The combining rule survives the move: groups are named
   sections, so "OR inside a group, AND between groups" stays legible. The
   inbox is the one screen wide enough to earn this, so the container widens
   for it alone. */
.inbox-layout {
  display: grid; grid-template-columns: 224px minmax(0, 1fr);
  gap: 16px 28px; align-items: start; margin-top: 14px;
}
/* Not sticky and never its own scroll region: the rail is taller than a laptop
   viewport, and an inner scrollbar would hide whole groups (Tempo's tail was
   invisible in the first cut) while looking complete. It flows with the page. */
.inbox-side {
  border-right: 1px solid var(--border); padding: 2px 22px 20px 0;
  display: flex; flex-direction: column; gap: 24px;
}
.side-head {
  display: flex; align-items: baseline; justify-content: space-between;
  font-size: 11px; color: var(--muted); text-transform: uppercase;
  letter-spacing: 0.08em; padding: 4px 6px 0;
}
.side-clear {
  border: none; background: none; font-family: inherit; cursor: pointer;
  color: var(--lilac); font-size: 11.5px; text-transform: none; letter-spacing: 0;
  padding: 0;
}
.side-clear:hover { color: var(--cream); }
.fgroup { display: flex; flex-direction: column; gap: 1px; }
.fgroup-name {
  font-size: 11px; color: var(--muted); text-transform: uppercase;
  letter-spacing: 0.07em; padding: 0 6px 5px;
}
.frow {
  display: flex; align-items: flex-start; gap: 8px; width: 100%;
  border: none; background: none; color: var(--text-2); font-family: inherit;
  font-size: 13px; padding: 5px 6px; border-radius: 8px; cursor: pointer;
  text-align: left;
}
.frow:hover { background: var(--surface-2); color: var(--text); }
.frow .fbox {
  flex: 0 0 auto; width: 14px; height: 14px; position: relative;
  background: var(--surface-2); border: 1px solid var(--border-2); border-radius: 4.5px;
}
.frow[aria-pressed="true"] { color: var(--text); }
.frow[aria-pressed="true"] .fbox { background: var(--invert-bg); border-color: var(--invert-bg); }
.frow[aria-pressed="true"] .fbox::after {
  content: ""; position: absolute; left: 3px; top: 3.5px; width: 6px; height: 3px;
  border: solid var(--invert-fg); border-width: 0 0 2px 2px; transform: rotate(-45deg);
}
/* 'one'-mode groups nest (Top 3 contains 1st), so their affordance is a radio:
   clicking a second replaces rather than accumulates, and the round mark says so. */
.fgroup-one .fbox { border-radius: 50%; }
.fgroup-one .frow[aria-pressed="true"] .fbox::after {
  left: 3.5px; top: 3.5px; width: 5px; height: 5px;
  border: none; border-radius: 50%; background: var(--invert-fg); transform: none;
}
/* Filter labels WRAP. The rail is 224px and leaves about 135px for the text, so
   the three mutually exclusive `via` options all clipped — "Tested against
   you,…" and "Came up in their…" stop distinguishing themselves, which is the
   entire job of a filter row. */
.frow-t { flex: 1; min-width: 0; white-space: normal; line-height: 1.3; }
.frow-n { color: var(--muted); font-size: 11.5px; font-variant-numeric: tabular-nums; }

/* An option nothing currently matches stays on screen and dims. Removing it
   would make the rail jump on every click, and "there are none of those among
   the ones you asked for" is the most useful thing a filter can say. */
.frow.f-empty { opacity: 0.38; }
.frow.f-empty[aria-pressed="true"] { opacity: 1; }

/* "Not measured yet" is a different KIND of answer from a measured one. Dashed,
   because the value is outstanding rather than negative — a solid muted row
   reads as "no vocal", which is precisely the claim this bucket exists to
   avoid making. */
.frow.f-unknown { color: var(--text-3); }
.frow.f-unknown .fbox { border-style: dashed; }
.frow.f-unknown[aria-pressed="true"] .fbox { background: var(--lilac); border-color: var(--lilac); }

/* Genre dots: one small hue per family, shared by the rail, the row strip and
   the roster key. Dots only, never a surface — --good and --danger keep their
   monopoly on meaning-bearing colour. */
.gdot { display: inline-block; flex: 0 0 auto; width: 7px; height: 7px; border-radius: 50%; }
.ib-sound .sb { display: inline-flex; align-items: center; gap: 5px; }

/* ---------- the roster key ---------- */
.inbox-top { display: flex; gap: 18px; align-items: flex-start; flex-wrap: wrap; }
.inbox-top .filters-row { flex: 1 1 320px; margin: 12px 0 8px; }
.fit-key {
  flex: 0 0 auto; min-width: 232px; max-width: 300px;
  border: 1px solid var(--border); border-radius: var(--r-ctl);
  padding: 11px 14px 9px; margin-top: 12px;
  display: flex; flex-direction: column; gap: 6px;
}
.fk-head {
  font-size: 11px; color: var(--muted); text-transform: uppercase;
  letter-spacing: 0.07em; margin-bottom: 2px;
}
.fk-row { display: flex; align-items: center; gap: 8px; font-size: 12.5px; }
.fk-dots { display: flex; gap: 3px; flex: 0 0 auto; min-width: 7px; }
.fk-name { flex: 1; min-width: 0; }
.fk-n { color: var(--muted); font-size: 11.5px; font-variant-numeric: tabular-nums; }
.fk-note { font-size: 11px; margin-top: 2px; }

@media (max-width: 940px) {
  .inbox-layout { grid-template-columns: 1fr; }
  .inbox-side {
    position: static; max-height: none; border-right: none;
    border-bottom: 1px solid var(--border); padding: 0 0 14px;
  }
  .fit-key { margin-top: 0; }
}

/* Kept for the legacy chip renderers still used elsewhere (queue, tags). */
.facets { display: flex; flex-direction: column; gap: 6px; }
.chip-btn.chip-empty { opacity: 0.38; }
.chip-btn.chip-empty.active { opacity: 1; }
.chip-btn.chip-unknown { border-style: dashed; color: var(--text-3); }
.chip-btn.chip-unknown.active { background: var(--lilac); border-color: var(--lilac); color: var(--invert-fg); }
.chip-clear { border-color: transparent; color: var(--muted); padding-left: 0; }
.chip-clear:hover { color: var(--cream); }

/* The disclosure holds reference copy. The marker is the affordance; the summary
   must not look like a link to somewhere else. */
.inbox-help { margin: 10px 0 12px; }
.inbox-help summary { cursor: pointer; width: fit-content; }
.inbox-help summary:hover { color: var(--cream); }
.inbox-help p { margin-top: 8px; }

.inbox-controls { align-items: center; }
.inbox-search { flex: 1 1 260px; min-width: 180px; padding: 8px 12px; font-size: 13px; }

@media (max-width: 720px) {
  /* The gutter label goes above its chips rather than shrinking: at 116px wide
     "How it arrived" wraps to three lines and pushes the row it names off screen. */
  .facet { grid-template-columns: 1fr; gap: 2px; }
  .facet-name { padding-top: 4px; }
}

.inbox-list { display: flex; flex-direction: column; }
.ib-list-col { display: flex; flex-direction: column; min-width: 0; }

/* ---------- the reading pane ----------
   Only when a row is open AND the viewport clears PANE_MQ (label.js): the list
   compacts to message rows on the left, the opened demo's detail draws in a
   sticky pane on the right — the same detailMarkup the inline expansion
   renders, in a different seat. Below the boundary the pane is never emitted,
   so none of this applies; the inline expansion under the row is the whole
   story there. */
.inbox-list.pane-open {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(300px, 360px);
  gap: 20px; align-items: start;
}
.ib-pane {
  border: 1px solid var(--border); border-radius: var(--r);
  background: var(--surface); padding: 14px 16px 18px;
  position: sticky; top: 14px;
  max-height: calc(100vh - 28px); overflow-y: auto;
}
.ib-pane-head {
  display: flex; align-items: flex-start; gap: 10px;
  padding-bottom: 10px; margin-bottom: 2px;
  border-bottom: 1px solid var(--border);
}
.ib-pane-id { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 2px; }
.ib-pane-title { font-size: 15px; font-weight: 600; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.ib-pane-sub { font-size: 12px; color: var(--muted); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.ib-pane-head .ib-band { flex: 0 0 auto; padding-top: 2px; }
.ib-pane-close {
  flex: 0 0 auto; border: none; background: none; cursor: pointer;
  color: var(--muted); font-family: inherit; font-size: 18px; line-height: 1;
  padding: 0 2px;
}
.ib-pane-close:hover { color: var(--text); }

/* While the pane is open the list is a message list, not a table: headers go,
   each row stacks sender/track over its sound strip with the match and status
   at the right — the same shape the narrow media query gives every row, which
   is the point: one compact grammar, two reasons to use it. */
.inbox-list.pane-open .ib-head { display: none; }
.inbox-list.pane-open .ib-main {
  grid-template-columns: 36px 1fr auto;
  grid-template-areas: "ava name status" "ava match when" "sound sound sound";
  row-gap: 4px;
}
.inbox-list.pane-open .ib-ava { grid-area: ava; }
.inbox-list.pane-open .ib-name { grid-area: name; }
.inbox-list.pane-open .ib-status { grid-area: status; }
.inbox-list.pane-open .ib-match { grid-area: match; flex-direction: row; align-items: baseline; gap: 8px; }
.inbox-list.pane-open .ib-when { grid-area: when; text-align: right; }
.inbox-list.pane-open .ib-sound { grid-area: sound; overflow-x: auto; padding-top: 2px; }

.ib-row { border-top: 1px solid var(--border-soft); }
/* The header bar above the first row already draws that line. */
.ib-head + .ib-row, .ib-row:first-child { border-top: none; }
.ib-row.open { background: var(--row-open); }
/* The column headers over the list — what makes it read as a table someone can
   work, not a feed. Same grid template as .ib-main below, or the labels drift
   off the columns they name. First column is the avatar gutter; Match holds
   the band with the placing beneath it (2026-08-28 message restyle — the row
   reads sender-first now, like a message, without losing a column of fact). */
.ib-head {
  display: grid;
  grid-template-columns: 36px minmax(0, 1fr) minmax(0, auto) 132px 92px 96px;
  gap: 12px; padding: 6px 8px;
  border-bottom: 1px solid var(--border);
  font-size: 10.5px; text-transform: uppercase; letter-spacing: 0.06em;
  color: var(--muted); font-weight: 600;
}
/* THE THREE FOLDS above the list (2026-10-02), where the archive door used to
   be one quiet right-aligned link. It is left-aligned and it is the same
   segmented control the demo door uses, because it is the same kind of thing:
   not a door to somewhere else, but which part of this list you are working.

   The count rides inside each button rather than beside it. Outside, it reads
   as a separate fact about the section; inside, it is part of the word — and
   it is what makes "Liked 0" worth pressing before anything is in it. */
.ib-folds { display: inline-flex; margin: 0 0 10px 8px; }
.ib-fold-n {
  margin-left: 7px; font-variant-numeric: tabular-nums; opacity: 0.55;
  font-size: 11.5px;
}
.seg-btn.active .ib-fold-n { opacity: 0.7; }
.ib-head span:last-child { text-align: right; }
.ib-main {
  width: 100%; display: grid;
  grid-template-columns: 36px minmax(0, 1fr) minmax(0, auto) 132px 92px 96px;
  gap: 12px; align-items: center; text-align: left;
  background: none; border: none; color: inherit; font-family: inherit;
  padding: 11px 8px; cursor: pointer;
}
.ib-main:hover { background: var(--surface-2); }

/* The sender's disc. INITIALS OF A FIELD THE ROW ALREADY PRINTS
   (submitted_artist), never of the published profile — the profile stays out
   of the list, see rowMarkup(). Neutral surface, no per-sender colour: colour
   on these screens is reserved for meaning (--good/--danger) and for the
   genre dots. */
.ib-ava {
  width: 32px; height: 32px; border-radius: 50%;
  display: inline-flex; align-items: center; justify-content: center;
  background: var(--surface-2); border: 1px solid var(--border-2);
  font-size: 11.5px; font-weight: 600; letter-spacing: 0.02em;
  color: var(--text-2); position: relative;
}
/* No name typed, no initials invented. Dashed — the same "different kind of
   answer" mark the unmeasured filter rows wear. */
.ib-ava.ava-none { border-style: dashed; }
/* Unread = still 'new' in triage. The dot rides the disc's corner so it reads
   at a glance without costing a column. */
.ib-row.unread .ib-ava::after {
  content: ""; position: absolute; top: -1px; right: -1px;
  width: 9px; height: 9px; border-radius: 50%;
  background: var(--lilac); border: 2px solid var(--surface);
}
.ib-row.unread .ib-from, .ib-row.unread .ib-title { color: var(--text); font-weight: 600; }

/* The Match cell: band over placing, two lines of the same fact family. */
.ib-match { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
.ib-band { display: inline-block; font-size: 13px; font-weight: 600; line-height: 1.25; }
.ib-band.b-high { color: var(--lilac); }
.ib-band.b-mid { color: var(--cream); }
.ib-band.b-low { color: var(--muted); }
/* Not-yet-scored takes the same slot at small size rather than leaving a dash.
   A dash in the band's slot reads as a rendering fault; two words read as a
   state, which is what it is. */
.ib-nyscore { color: var(--muted); text-transform: lowercase; font-size: 11px; line-height: 1.2; }

/* The sound strip: only what has been measured. An absent value contributes
   nothing at all — a row of dashes on every untagged demo buries the tagged ones,
   and the detail panel is where absence gets stated in words. */
.ib-sound { display: flex; gap: 5px; flex-wrap: nowrap; overflow: hidden; }
.ib-sound .sb {
  font-size: 11.5px; color: var(--muted); white-space: nowrap;
  border: 1px solid var(--border); border-radius: var(--r-chip); padding: 2px 7px;
  font-variant-numeric: tabular-nums;
}
.ib-sound .sb em { font-style: normal; opacity: 0.55; margin-left: 2px; }
/* The one badge on this row that is a finding rather than a measurement, so the
   one that is allowed to carry colour. Nothing marks the absence of a marker —
   see soundStrip() for why there is no green counterpart. */
.ib-sound .sb-ai {
  color: var(--danger); border-color: var(--danger);
  background: rgba(217, 106, 106, 0.10); font-weight: 600;
}
.ib-facts .v-ai { color: var(--danger); }
/* The released-record match (054). Same colour as the AI marker on purpose —
   both are "worth a look before you pass" flags — but its own class, so a
   restyle of one cannot silently restyle the other. */
.ib-facts .v-released { color: var(--danger); }
/* The producer's dispute of that match (074): their claim, set beside the flag
   in the amber this product already uses for "somebody else's statement". */
.ib-facts .v-disputed { color: #d79a3a; }  /* the amber Foundry uses for another party's statement; not a variable on this stylesheet */

/* The cover in the opened detail. A fixed square: covers arrive in every
   aspect ratio and the panel must not reflow around each one. Sized to be
   read, not to dominate — the score above it is still the subject. */
.ib-art { margin: 2px 0 4px; }
.ib-art-img {
  display: block; width: 148px; height: 148px; object-fit: cover;
  border-radius: 10px; border: 1px solid var(--border);
}
/* Smaller in the reading pane: the pane is ~340px and the profile sits beside
   the cover — at 148px the pair wrapped on every open. */
.ib-pane .ib-art-img { width: 112px; height: 112px; }

.ib-sound-panel { border-top: 1px solid var(--border); padding-top: 10px; }
.ib-unscored-note { border-left: 2px solid var(--border); padding-left: 10px; }
.ib-name { min-width: 0; display: flex; flex-direction: column; gap: 2px; }
/* Sender first, track under it — the message reading order. The sender line
   is a shade quieter than the title so an unnamed row (title only) does not
   look like a different kind of row. */
.ib-from { font-size: 13px; color: var(--text-2); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.ib-title { font-size: 14.5px; font-weight: 500; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.untitled { color: var(--muted); font-style: normal; }
.ib-place, .ib-when { font-size: 12.5px; color: var(--muted); white-space: nowrap; }
.ib-status {
  font-size: 12px; text-align: right; text-transform: lowercase;
  color: var(--muted); white-space: nowrap;
}
.ib-status.s-new { color: var(--cream); }
.ib-status.s-shortlisted { color: var(--good); }
.ib-status.s-passed { color: var(--text-3); }

.ib-detail { padding: 4px 6px 18px; display: flex; flex-direction: column; gap: 12px; }
.ib-spread-head { display: flex; justify-content: space-between; font-size: 12.5px; margin-bottom: 6px; }
.ib-facts { display: grid; grid-template-columns: repeat(auto-fit, minmax(230px, 1fr)); gap: 0 18px; }
.ib-audio { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; margin-bottom: 10px; }
.ib-audio .k { color: var(--muted); font-size: 12.5px; }
.ib-triage { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }
.ib-triage .k { color: var(--muted); font-size: 12.5px; }
.seg { display: flex; gap: 2px; background: var(--surface-2); border: 1px solid var(--border); border-radius: var(--r-ctl); padding: 3px; }
.seg-btn {
  font-family: inherit; font-size: 12.5px; cursor: pointer; border: none; background: none;
  color: var(--muted); padding: 5px 13px; border-radius: var(--r-chip);
}
.seg-btn.active { background: var(--invert-bg); color: var(--invert-fg); }
.ib-note { flex: 1 1 220px; min-width: 160px; padding: 8px 10px; font-size: 13px; }
.ib-saved { font-size: 12px; color: var(--muted); min-width: 44px; }

/* ---------- empty states ----------
   An empty inbox is a real state and must not look like a failure. Same surface
   as everything else, no icon, no red. */
.empty-state {
  border: 1px dashed var(--border-2); border-radius: var(--r); padding: 22px;
  display: flex; flex-direction: column; gap: 6px;
}
.empty-head { font-size: 14.5px; font-weight: 500; }
.empty-note { padding: 16px 2px; }

/* ---------- access codes ---------- */
.issue-row {
  display: grid; grid-template-columns: 180px 200px 1fr auto; gap: 8px;
  align-items: center; margin: 14px 0 10px;
}
.issue-row .set-select, .issue-row input { width: 100%; }
.issued { font-size: 13px; margin-bottom: 12px; display: flex; flex-direction: column; gap: 4px; }
.code-out {
  font-size: 18px; letter-spacing: 1px; color: var(--cream);
  background: var(--surface-2); border: 1px solid var(--border);
  border-radius: var(--r-ctl); padding: 10px 14px; align-self: flex-start; user-select: all;
}
.row-off td { opacity: 0.5; }
.chip.bad { color: var(--danger); border-color: rgba(217, 106, 106, 0.35); }

/* ---------- narrow ----------
   The inbox row is five columns of information; below ~700px it becomes two
   rows rather than five squeezed columns, and the table blocks keep their own
   horizontal scroll so the page itself never scrolls sideways. */
@media (max-width: 700px) {
  .card { padding: 14px 14px; }
  /* The header bar names columns the narrow grid no longer has. */
  .ib-head { display: none; }
  /* THESE AREAS NAMED A ROW THAT NO LONGER EXISTS, and that is why a phone drew
     the list with holes in it. The template was `"ava name status" "ava match
     when" "sound sound sound"` — the 2026-08-28 message row, with a separate
     sound strip. The 2026-09-09 restyle replaced that row with art / name /
     wave / match / when and folded the strip's facts into .ib-meta; soundStrip()
     has not been called by anything since. So on a phone .ib-art and .ib-wave
     had no area, auto-placed into whatever cells were left, and every row came
     out double height with the play button floating below its own title
     (measured 2026-10-01, tools/shoot_inbox_local.mjs).

     Two areas, for the two things the row actually has at this width: the disc,
     and the name with the band beneath it. The wave and the date stay hidden
     (the ≤900px block), because 390px cannot hold them and a 40px waveform is
     not a waveform. */
  .ib-main {
    grid-template-columns: 44px minmax(0, 1fr);
    grid-template-areas: "art name" "art match";
    row-gap: 2px;
  }
  .ib-art, .ib-ava { grid-area: art; }
  .ib-name { grid-area: name; }
  .ib-match { grid-area: match; flex-direction: row; align-items: baseline; gap: 8px; }
  .issue-row { grid-template-columns: 1fr; }
  .stat-row { grid-template-columns: repeat(auto-fit, minmax(130px, 1fr)); }
  .stat-n { font-size: 20px; }
  .kv > div, .ib-facts > div { gap: 8px; }
}

/* ---------- Who sent this ------------------------------------------------
   The head of the opened detail since the 2026-08-28 message restyle: the
   cover (when one exists) and the published profile sit side by side, wrapping
   to a stack when the pane or a phone leaves no room. Still deliberately
   quieter than the score — smaller type, no colour of its own — because the
   score has already been read by the time this loads (see detailMarkup), and
   this is context for it, not a headline. */
.ib-detail-top { display: flex; gap: 14px; align-items: flex-start; flex-wrap: wrap; }
.ib-detail-top .ib-art { flex: 0 0 auto; margin: 0; }
.ib-detail-top .ib-producer { flex: 1 1 240px; min-width: 0; padding: 0; border-top: none; }
.ib-producer { display: grid; gap: 0.5rem; padding: 0.9rem 0; border-top: 1px solid var(--border); }
.pd { display: grid; gap: 0.55rem; }
.pd-head { display: flex; align-items: center; gap: 0.65rem; flex-wrap: wrap; }
.pd-head strong { font-size: 1rem; font-weight: 500; }
/* The profile's own disc — initials of the published name, no image: the
   schema stores none (migration 014), so there is nothing to fetch and no
   better-funded look to compete on. Dashed when no name was published, like
   the row's. */
.pd-ava {
  flex: 0 0 auto; width: 36px; height: 36px; border-radius: 50%;
  display: inline-flex; align-items: center; justify-content: center;
  background: var(--surface-2); border: 1px solid var(--border-2);
  font-size: 12.5px; font-weight: 600; letter-spacing: 0.02em; color: var(--text-2);
}
.pd-ava.ava-none { border-style: dashed; }
.pd-genres { display: flex; flex-wrap: wrap; gap: 0.4rem; }
.pd-bio { margin: 0; max-width: 44rem; color: var(--text-2); font-size: 0.88rem; line-height: 1.6; }
.pd-links { display: flex; flex-wrap: wrap; gap: 0.5rem; }
.pd-link {
    padding: 0.28rem 0.6rem;
    border: 1px solid var(--border);
    border-radius: var(--r-chip);
    color: var(--text-2);
    font-size: 0.8rem;
    text-transform: capitalize;
    text-decoration: none;
}
.pd-link:hover { border-color: var(--text-3); color: var(--text); }
/* Said every time the links are shown, not once in a help page. A label acting
   on a SoundCloud URL should know nobody verified whose it is. */
.pd-claim { margin: 0; color: var(--text-3); font-size: 0.76rem; }

/* ---------- the thread (migration 038) ----------
   Same visual weight as the producer panel above it: evidence in the detail,
   never on the list row. The two sides differ by a left rule, not by bubbles —
   this is correspondence about a demo, not a chat product. */
.ib-msgs { display: grid; gap: 0.5rem; padding: 0.9rem 0; border-top: 1px solid var(--border); }
.msg-list { display: grid; gap: 0.55rem; max-width: 44rem; }
.msg { display: grid; gap: 2px; padding-left: 0.7rem; border-left: 2px solid var(--border); }
.msg-you { border-left-color: var(--lilac); }
.msg-who { color: var(--text-3); font-size: 0.72rem; text-transform: uppercase; letter-spacing: 0.04em; }
.msg-body { color: var(--text-2); font-size: 0.88rem; line-height: 1.55; white-space: pre-wrap; overflow-wrap: anywhere; }
.msg-empty { margin: 0; }
.msg-compose { display: flex; align-items: flex-start; gap: 10px; flex-wrap: wrap; max-width: 44rem; }
.msg-box { resize: vertical; font-family: inherit; }


/* ---------- the rail below 900px ----------
   A top bar, foundry-fashion: the destinations are how you get out of a long
   inbox on a phone, so they stay sticky and scroll sideways as tabs. */
@media (max-width: 900px) {
  #shell { flex-direction: column; }
  .side {
    flex: none; width: 100%; height: auto; position: sticky; z-index: 30;
    flex-direction: row; flex-wrap: wrap; align-items: center;
    border-right: none; border-bottom: 1px solid var(--border);
    padding: 12px 14px 0; overflow: visible;
  }
  .side-top { flex: 1; padding: 0 0 12px; }
  .side-foot {
    margin-top: 0; padding: 0 0 12px; border-top: none;
    flex-direction: row; align-items: center;
  }
  /* The rail is a top bar down here, so the menu drops DOWN from the chip
     instead of opening upward off the bottom of the screen. */
  .account-menu { bottom: auto; top: calc(100% + 8px); left: auto; right: 0; }
  /* The rail is a wrapping row down here, so its tabs can outrun the screen.
     Scroll them rather than clipping them: a destination you cannot reach is
     worse than one you have to swipe to. */
  .side-nav { overflow-x: auto; flex-wrap: nowrap; -webkit-overflow-scrolling: touch;
    scrollbar-width: none; }
  .side-nav::-webkit-scrollbar { display: none; }
  .side-nav .pill { flex: 0 0 auto; }
  .account-who { max-width: 140px; }
  .side-nav {
    order: 3; width: 100%; flex-direction: row; gap: 4px;
    overflow-x: auto; scrollbar-width: none;
  }
  .side-nav::-webkit-scrollbar { display: none; }
  /* width:auto is load-bearing: a flex row of 100%-wide items is four
     full-viewport tabs with only the first on screen. */
  .pill { flex: none; width: auto; height: 34px; min-height: 34px; padding: 0 12px; border-radius: 8px; }
  .rail-group { display: contents; }
  .rail-sec { display: none; }
}

/* ---------- the door ----------
   Taking demos / Paused, in the segmented control triage already uses, with
   the consequence one press away behind the info mark. The row hides until the
   inbox response says which state is true - a switch drawn before the answer
   would show a state nobody asserted. */
.door-row {
  display: flex; align-items: center; gap: 10px; flex-wrap: wrap;
  margin: 2px 0 12px;
}
/* The current side is DISABLED (there is nothing to press) but must stay the
   loudest thing in the control — dimming it below the pressable side is what
   made the switch read as two labels and no state. Only a non-active disabled
   segment (transient, mid-request) dims. */
.door-row .seg-btn:disabled { cursor: default; }
.door-row .seg-btn:disabled:not(.active) { opacity: 0.7; }
.door-row .seg-btn.active:disabled { opacity: 1; }
.info-dot {
  display: inline-flex; align-items: center; justify-content: center;
  width: 18px; height: 18px; border-radius: 50%;
  border: 1px solid var(--border-2); color: var(--muted);
  font-size: 11px; font-style: normal; cursor: pointer; list-style: none;
  user-select: none;
}
.info-dot::-webkit-details-marker { display: none; }
.door-info { position: relative; display: inline-flex; }
.door-info[open] .info-dot { color: var(--text); border-color: var(--text-3); }
.door-info[open] > p {
  position: absolute; z-index: 40; top: 24px; left: -9px;
  width: min(340px, 78vw);
  background: var(--surface); border: 1px solid var(--border);
  border-radius: 10px; padding: 10px 12px; margin: 0;
}

/* ---------- advanced ----------
   The technical half of "Your sound", folded (Nikhil: "it should be in an
   advanced section"). Same quiet disclosure the inbox help uses: the summary
   must not read as a link to somewhere else. */
.advanced { margin-top: 4px; }
.advanced > summary {
  cursor: pointer; width: fit-content; color: var(--muted); font-size: 12.5px;
}
.advanced > summary:hover { color: var(--cream); }
.advanced[open] > summary { margin-bottom: 10px; }
.advanced .card { margin-top: 12px; }
.advanced .kv { margin-top: 4px; }

/* A held row's clock, where the status chip sits on a delivered one. */
.ib-status.s-waiting { color: var(--cream); }


.door-row { margin: 2px 0 6px; }

/* The photograph CTA that stood here ("the crate") was removed on 2026-08-26 —
   a billboard inside a working console was the loudest thing on the screen and
   said nothing operational. The road to the technical story survives as the
   one-line link under the inbox list; /img/platform-cta.jpg is still served
   for anything that links it directly. */

/* The in-place demo player (migration 052), mounted under the audio row when
   Play is pressed. Empty until then, and invisible while empty. */
.ib-player:empty { display: none; }
.ib-player { margin: 8px 0 4px; }
.ib-player-el { width: 100%; height: 36px; }

/* The organization inbox (migration 057). Rows are read-only here — triage
   lives in the workspace inbox — so the layout is a queue, not a console. Drawn
   as a table with named columns, same discipline as the workspace inbox: the
   raw `via` slugs and date-dot-status runs it used to print were the single
   most confusing thing on the org screen (Nikhil, 2026-08-26). The fit chips
   carry the org-only comparison; the chip for the workspace the demo was
   actually sent to is outlined so "where it went" and "where it fits" read
   apart at a glance. */
.org-inbox-head, .org-inbox-row {
  display: grid; gap: 12px; align-items: center;
  grid-template-columns: minmax(180px, 1.4fr) minmax(120px, 1fr) 150px 90px 82px minmax(160px, 1fr);
  padding: 9px 8px; border-top: 1px solid var(--border-soft);
}
.org-inbox-head {
  border-top: none; border-bottom: 1px solid var(--border); padding: 6px 8px;
  font-size: 10.5px; text-transform: uppercase; letter-spacing: 0.06em;
  color: var(--muted); font-weight: 600;
}
.org-inbox-head + .org-inbox-row { border-top: none; }
.org-inbox-head span:last-child { text-align: right; }
.oir-name {
  font-weight: 500; font-size: 13.5px; min-width: 0;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.oir-cell { font-size: 12.5px; color: var(--text-2); min-width: 0;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.oir-cell.muted { color: var(--muted); }
.oir-fits { display: flex; gap: 6px; flex-wrap: wrap; justify-content: flex-end; }
@media (max-width: 900px) {
  .org-inbox-head { display: none; }
  .org-inbox-row { grid-template-columns: 1fr; gap: 3px; padding: 10px 4px; }
  /* The header is gone, so each stacked value carries its own label — a bare
     "Aug 26, 2026" over a bare "new" identifies nothing. */
  .oir-cell[data-l]::before { content: attr(data-l) ': '; color: var(--muted); }
  .oir-fits { justify-content: flex-start; }
}
.org-fit-chip {
  font-size: 12px; padding: 2px 8px; border-radius: var(--r-chip);
  background: rgba(127,127,127,.12); white-space: nowrap;
  font-variant-numeric: tabular-nums;
}
.org-fit-chip.own { outline: 1px solid currentColor; outline-offset: -1px; }

/* ---------- the structure tree (owner console, 2026-08-28) ---------- */
.struct-org { border-top: 1px solid var(--border); padding: 4px 0; }
.struct-org:first-child { border-top: none; }
.struct-org summary { cursor: pointer; list-style: none; padding: 9px 2px; }
.struct-org summary::-webkit-details-marker { display: none; }
.struct-org summary::before { content: '+ '; color: var(--muted); }
.struct-org[open] summary::before { content: '\2212 '; }
.struct-sum { margin-left: 8px; }
.struct-cap { font-size: 11px; text-transform: uppercase; letter-spacing: 0.8px;
              color: var(--dim, var(--muted)); margin: 10px 0 4px 18px; }
.struct-row { display: flex; gap: 12px; align-items: baseline; flex-wrap: wrap;
              padding: 4px 2px 4px 18px; font-size: 14px; }
.struct-code { font-size: 12.5px; color: var(--text-2); }

/* ---------- the nine71charts review queue ----------
   One card per claim, in three columns that answer three different questions:
   what was said, what the corpus already says about that cue, and who said it.
   A table row was tried first and could not carry the middle column — which is
   the one most often forgotten, and a decision made without it is a decision
   made against a blank. */
.facet-row { display: flex; flex-wrap: wrap; gap: 6px; align-items: center; margin-bottom: 14px; }
.facet-sep { width: 1px; height: 18px; background: var(--border); margin: 0 6px; }

.claim-list { display: flex; flex-direction: column; gap: 12px; }
.claim {
  display: grid; gap: 14px 22px; align-items: start;
  grid-template-columns: minmax(0, 1.3fr) minmax(0, 1fr) minmax(0, 1fr);
  padding: 14px 16px; border: 1px solid var(--border); border-radius: 12px;
  background: var(--surface-2);
}
.claim.is-busy { opacity: 0.45; pointer-events: none; }
.claim-kind { font-size: 12.5px; color: var(--muted); text-transform: uppercase; letter-spacing: 0.07em; }
.claim-what { margin-top: 4px; font-size: 15px; }
.claim-note { margin-top: 4px; }
.claim-tag {
  display: inline-block; margin-left: 6px; padding: 1px 7px; border-radius: 999px;
  border: 1px solid var(--border); font-size: 11px; letter-spacing: 0.04em;
  text-transform: none; color: var(--muted);
}
/* Gold, the colour this product already uses for a tentative claim. A self-claim
   is not a warning and must not be red: it is the row that has to be checked. */
.claim-tag-own { border-color: var(--gold, #d8a13a); color: var(--gold, #d8a13a); }
.claim-evidence { color: var(--gold, #d8a13a); font-size: 13px; line-height: 1.5; }
.claim-actions { grid-column: 1 / -1; display: flex; flex-wrap: wrap; gap: 8px; align-items: center; }
.claim-actions .claim-note { flex: 1 1 240px; }
input.claim-note {
  font-family: inherit; font-size: 13px; padding: 7px 10px;
  background: var(--surface); color: inherit;
  border: 1px solid var(--border); border-radius: 8px;
}
.alarm-note { margin-top: 12px; line-height: 1.6; }
.alarm-note code { font-size: 12.5px; }
.claim-audio { margin-top: 8px; }
.claim-audio audio { width: 100%; max-width: 320px; }

/* A checkbox that sits on the same row as the inputs beside it. The attribution
   panel's "unreleased" is the only one, and it belongs on that row rather than
   under it: it is part of the same statement. */
.check-inline { display: inline-flex; align-items: center; gap: 6px; font-size: 13px; color: var(--muted); }

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

/* ======================================================================
   THE INBOX AS A LIST OF RECORDS (2026-09-09). Art, title and facts, the
   waveform, the match, when it arrived; then the acts as quiet round icons
   on the right: play, shortlist, pass, comment, download. Black on white by
   default; the filter rail opens and closes. */
/* ONE TEMPLATE FOR EVERY ROW, and this was the list's worst fault.

   The acts cluster sized itself (`auto`). A team upload carries ONE control
   where a demo carries five, so the 1fr holding the title, the wave, the match
   and the date came out ~150px wider on a team row than on the demo row below
   it: no column in the list lined up with the one above, and on the demo row
   the waveform ran under the word "High match". The header's trailing 200px
   was tuned to the five-icon row, which is exactly why the list looked square
   until the day a team upload appeared in it (2026-09-17, the merge).

   Fixed track, sized to the widest cluster (5 × 34px + gaps + the 10px
   right pad). The play button therefore sits at the same x on every row,
   which is the control a label presses all afternoon. */
:root { --ib-acts: 190px; }
.ib-line { display: grid; grid-template-columns: minmax(0, 1fr) var(--ib-acts); align-items: center; gap: 8px; }
/* `when` was 56px holding "13 days ago", which needs 67 — so the date wrapped
   or spilled on every row in the list. Taken from `match`, which holds two
   words and had room. */
/* The name takes the lion's share of the flexible space (1.7fr against 1fr).
   At 1.1/1 the Track column was 194px on a 1480px screen: the title truncated
   mid-word and the meta line ended at "Team upload · Ky William · 1…", so the
   tempo, the key and the genre — the facts the row exists to show — were never
   visible at all while a 176px waveform sat beside them. */
.ib-main { grid-template-columns: 44px minmax(0, 1.7fr) minmax(96px, 1fr) 104px 76px; gap: 14px; padding: 10px 8px; }
/* The head is one grid of six where the row is a grid of five beside a fixed
   acts track, and the two gaps differ (14 inside the row, 8 beside it). So the
   head's trailing column is the acts width less that 6px of difference —
   without it every header label sat 14px left of the column it names. */
.ib-head { grid-template-columns: 44px minmax(0, 1.7fr) minmax(96px, 1fr) 104px 76px calc(var(--ib-acts) - 6px); gap: 14px; }
.ib-art { width: 44px; height: 44px; border-radius: 8px; overflow: hidden; display: inline-flex; align-items: center; justify-content: center; background: var(--surface-2); }
.ib-art-img { width: 100%; height: 100%; object-fit: cover; display: block; }
.ib-art .ib-ava { width: 44px; height: 44px; border-radius: 8px; font-size: 13px; }
.ib-name { display: flex; flex-direction: column; gap: 3px; min-width: 0; }
.ib-title { font-size: 14.5px; font-weight: 600; }
.ib-meta { font-size: 12.5px; color: var(--muted); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.ib-dot { margin: 0 6px; color: var(--text-3); }
/* overflow:hidden is the backstop, not the fix — rowBars() in label.js draws a
   count that fits. This stops a narrow window from putting the tail of the
   strip under the match band again. */
.ib-wave { display: flex; align-items: center; gap: 1.5px; height: 34px; min-width: 0; overflow: hidden; }
.ib-wave i { flex: 1 1 0; min-width: 1px; background: var(--text-3); opacity: 0.55; border-radius: 1px; }
.ib-row.open .ib-wave i, .ib-row:hover .ib-wave i { opacity: 0.85; }
.ib-row.passed .ib-wave i { opacity: 0.25; }
/* flex-start, not flex-end: the acts sit in a fixed track now, and aligning
   them to its right edge would put PLAY under the download icon of the row
   above. Left-aligned, play is the same target on every row and the rows that
   carry more controls simply extend further right. */
.ib-acts { display: inline-flex; align-items: center; justify-content: flex-start; gap: 2px; padding-right: 10px; }
.ib-ico { width: 34px; height: 34px; border-radius: 50%; border: none; background: none; color: var(--text-2); display: inline-flex; align-items: center; justify-content: center; cursor: pointer; padding: 0; }
.ib-ico:hover { background: var(--surface-2); color: var(--text); }
.ib-ico.on { color: var(--text); background: var(--surface-2); }
.ib-ico.on-heart { color: #e0245e; }
.ib-ico:disabled { opacity: 0.5; cursor: default; }
.ib-when { font-size: 12px; color: var(--muted); white-space: nowrap; }
.ib-nyscore { font-size: 12px; color: var(--muted); }
.ib-row.passed .ib-title, .ib-row.passed .ib-meta { opacity: 0.6; }
.inbox-layout.filters-hidden { grid-template-columns: minmax(0, 1fr); }
.inbox-layout.filters-hidden .inbox-side { display: none; }
.f-exact-row { display: flex; align-items: center; justify-content: space-between; gap: 10px; padding: 6px 0 2px; }
.f-exact { width: 88px; padding: 6px 8px; border: 1px solid var(--border-2); border-radius: var(--r-ctl); background: var(--surface); color: var(--text); font: inherit; font-size: 13px; }
:root[data-theme="light"] .btn.primary { background: #111; color: #fff; }
@media (max-width: 900px) {
  /* THE ACTS DROP TO THEIR OWN LINE. Beside the row they took ~180px of a
     390px screen on a demo (five controls), which left the title column with
     nothing: the track's name did not render at all and "High match" wrapped
     over two lines. The console is a desktop tool and does not need a mobile
     redesign (Nikhil, 2026-09-17: "it just has to work") — but a row with no
     title on it is not working. Stacked, the name has the full width and the
     controls sit under it, right-aligned where the thumb is. */
  :root { --ib-acts: auto; }
  .ib-line { grid-template-columns: minmax(0, 1fr); row-gap: 0; }
  .ib-acts { justify-content: flex-end; padding: 0 10px 10px; }
  .ib-main { grid-template-columns: 44px minmax(0, 1fr) 96px; padding-bottom: 4px; }
  .ib-wave, .ib-when, .ib-head { display: none; }
}

/* Row play: a black disc with a white glyph, the one filled control on the row. */
.ib-ico.ib-play { background: var(--text); color: var(--bg); }
.ib-ico.ib-play:hover { background: var(--text); color: var(--bg); opacity: 0.85; }
.ib-ico.ib-play.on { background: var(--text); color: var(--bg); }
select.f-exact { width: auto; max-width: 150px; }

/* ======================================================================
   THE WHOLE PAGE, AN OVERLAY FOR THE OPENED TRACK, A BOTTOM PLAYER
   (Nikhil, 2026-09-09). */
main { max-width: none; padding-inline: clamp(16px, 2.5vw, 40px); }
body.has-player main { padding-bottom: 96px; }
.ib-overlay { position: fixed; inset: 0; z-index: 60; display: flex; align-items: flex-start; justify-content: center; padding: 4vh 16px; }
.ib-overlay-backdrop { position: absolute; inset: 0; background: rgba(10, 10, 14, 0.42); }
.ib-overlay-card {
  position: relative; width: min(1040px, 96vw); max-height: 92vh; overflow: auto;
  background: var(--surface); color: var(--text); border: 1px solid var(--border);
  border-radius: 16px; box-shadow: 0 24px 80px rgba(0, 0, 0, 0.35); padding: 6px 28px 28px;
}
.ib-overlay-card .ib-pane { width: auto; max-width: none; border: none; padding: 0; position: static; height: auto; max-height: none; overflow: visible; }
.ib-overlay-card .ib-pane-head { position: sticky; top: 0; background: var(--surface); z-index: 1; padding: 18px 0 12px; }
.ib-overlay-card .ib-detail { font-size: 14.5px; }
.ib-overlay-card .ib-detail p, .ib-overlay-card .ib-detail .muted { max-width: 72ch; }
body.ib-overlay-open { overflow: hidden; }
.player-bar {
  position: fixed; left: 0; right: 0; bottom: 0; z-index: 55; height: 72px;
  display: grid; grid-template-columns: 52px minmax(160px, 320px) 44px minmax(0, 1fr) 96px 36px;
  align-items: center; gap: 14px; padding: 0 20px;
  background: var(--surface); border-top: 1px solid var(--border);
  box-shadow: 0 -8px 30px rgba(0, 0, 0, 0.08);
}
.pb-art { width: 52px; height: 52px; border-radius: 8px; overflow: hidden; background: var(--surface-2); border: none; padding: 0; cursor: pointer; display: inline-flex; align-items: center; justify-content: center; }
.pb-art img { width: 100%; height: 100%; object-fit: cover; display: block; }
.pb-art .ib-ava { width: 52px; height: 52px; border-radius: 8px; }
.pb-id { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
.pb-title { font-weight: 600; font-size: 14px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.pb-sub { font-size: 12px; color: var(--muted); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.pb-toggle { width: 44px; height: 44px; border-radius: 50%; border: none; background: var(--text); color: var(--bg); display: inline-flex; align-items: center; justify-content: center; cursor: pointer; padding: 0; }
.pb-track { height: 6px; border-radius: 3px; background: var(--surface-2); cursor: pointer; position: relative; }
.pb-fill { position: absolute; left: 0; top: 0; bottom: 0; width: 0; border-radius: 3px; background: var(--text); }
.pb-time { font-size: 12px; color: var(--muted); font-variant-numeric: tabular-nums; text-align: right; }
.pb-close { width: 36px; height: 36px; border-radius: 50%; border: none; background: none; color: var(--muted); font-size: 22px; cursor: pointer; }
.pb-close:hover { background: var(--surface-2); color: var(--text); }
.ib-ico.on { color: var(--text); background: var(--surface-2); }
@media (max-width: 760px) {
  .player-bar { grid-template-columns: 44px minmax(0, 1fr) 40px 36px; height: 64px; }
  .pb-track, .pb-time { display: none; }
}

/* ---------- the label's own uploads (086-090) ----------
   Batches, because a label thinks in "Justin sent twelve on Tuesday"; the
   uploader's note belongs to the press of Upload, not to each track. Rows reuse
   the inbox's band words (.ib-band) so a High match reads the same on both
   screens. The org table reuses .org-inbox-row's grid with its own headers. */
.intake-batch { border-top: 1px solid var(--border); padding: 10px 0 6px; }
.intake-batch:first-child { border-top: none; }
.intake-batch-head { display: flex; justify-content: space-between; gap: 12px; align-items: baseline; padding: 2px 8px 6px; }
.intake-who { font-weight: 600; font-size: 13.5px; }
.intake-note { margin: 0 8px 8px; padding: 8px 10px; font-size: 12.5px; color: var(--text-2);
  background: var(--surface-2); border-radius: var(--r-chip, 8px); white-space: pre-wrap; }
.intake-row { display: grid; gap: 10px; align-items: center; padding: 6px 8px;
  grid-template-columns: minmax(160px, 1.6fr) minmax(0, 1fr) minmax(140px, 1fr) 92px auto; }
.intake-row.slim { grid-template-columns: minmax(160px, 1.6fr) minmax(120px, 1fr); }
.ir-name { font-size: 13.5px; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.ir-cell { font-size: 12.5px; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.ir-act { text-align: right; }
.ir-remove { padding: 2px 7px; margin-left: 6px; line-height: 1; }
@media (max-width: 900px) {
  .intake-row { grid-template-columns: 1fr 1fr; }
  .ir-act { grid-column: span 2; }
}
.inline-form.wrap { flex-wrap: wrap; align-items: center; }
.inline-form.wrap input[type=text] { flex: 1 1 220px; }
.minted { margin-top: 12px; padding: 12px 14px; border: 1px solid var(--lilac); border-radius: var(--r); }
.code-box { display: flex; flex-direction: column; gap: 6px; margin: 8px 0; }
.code-line { display: grid; grid-template-columns: 48px 1fr; gap: 10px; align-items: baseline; font-size: 13.5px; }
.code-str { font-size: 16px; font-weight: 600; letter-spacing: 0.02em; user-select: all; }
.org-fit-chip.b-high { outline-color: var(--lilac); color: var(--lilac); }
.org-fit-chip.b-low { color: var(--muted); }

/* The measured upload (091): one detail band under each intake row, on both
   screens. Facts read as a wrapped list of short cells; the sentence sits
   under them in the secondary colour; the neighbours are the quietest line,
   because a cosine is a pointer to go and listen, not a verdict. Anything the
   product has not measured is the dashed "not measured yet" cell and never a
   zero or a blank. */
.ir-detail, .oir-detail { grid-column: 1 / -1; min-width: 0; display: flex; flex-direction: column;
  gap: 10px; padding: 8px 0 12px; font-size: 12.5px; color: var(--text-2); }
.intake-row .ir-detail { margin-top: 2px; }

/* THE FACT BAND IS A ROW OF SMALL STATS, not a sentence of run-together pairs.
   It used to be inline label+value with 4px between them, which at seven facts
   read as "GenreTech house BPM128 Key5A C minor" — the label and the value ran
   into each other and nothing had a shape. Each fact is now a stacked cell: a
   small muted caption over the value, on a grid that wraps. Same facts, same
   honesty rules, legible at a glance. */
.ir-facts { display: flex; flex-wrap: wrap; gap: 10px 22px; align-items: flex-start; }
/* No nowrap. These are stacked caption-over-value cells in a WRAPPING flex row,
   so nowrap protected nothing and, with no overflow rule beside it, let a long
   value spill across its neighbour instead of wrapping. */
.ir-fact { display: flex; flex-direction: column; gap: 1px; min-width: 0; }
.ir-fact .k { font-size: 10px; text-transform: uppercase; letter-spacing: .06em;
  color: var(--muted); font-weight: 500; }
.ir-fact > :not(.k) { font-size: 13px; color: var(--text); }
/* An unmeasured value keeps the dashed underline it always had — the signal
   that this is an absence, not a reading — but only under the value. */
.ir-fact.unmeasured { color: var(--muted); border-bottom: 0; }
.ir-fact.unmeasured > :not(.k) { color: var(--muted); font-size: 12px;
  border-bottom: 1px dashed var(--border-2, var(--border)); align-self: flex-start; }
.ir-fact.pending { color: var(--muted); }
.ir-fact.pending > :not(.k) { color: var(--muted); font-size: 12px; font-style: italic; }
.ir-fact.is-error { color: var(--danger, #b4462f); }

.ir-words { color: var(--text-2); line-height: 1.45; max-width: 68ch; }

/* NEIGHBOURS ON THEIR OWN LINES. Five of them comma-run with the cosine jammed
   against the artist ("Pedroz, Abbud0.88") was the densest thing on the screen
   and the one an A&R actually reads. One per line, the cosine right-aligned in
   a fixed column so the numbers stack and can be compared down the list. */
.ir-near { color: var(--muted); line-height: 1.45; }
.ir-near-head { font-size: 10px; text-transform: uppercase; letter-spacing: .06em;
  color: var(--muted); font-weight: 500; margin-bottom: 3px; }
.ir-nlist { display: flex; flex-direction: column; gap: 2px; max-width: 54ch; }
.ir-nrow { display: flex; align-items: baseline; gap: 8px; }
.ir-nrow .who { color: var(--text-2); min-width: 0; overflow: hidden;
  text-overflow: ellipsis; white-space: nowrap; }
.ir-nrow .who b { color: var(--text); font-weight: 550; }
.ir-nrow .lab { color: var(--muted); }
.ir-nrow .cos { margin-left: auto; font-family: var(--mono, ui-monospace, monospace);
  font-size: 11.5px; color: var(--muted); font-variant-numeric: tabular-nums; }
.ir-near.pending { font-style: italic; }
.ir-stems { display: flex; align-items: baseline; gap: 8px; flex-wrap: wrap; }

/* THE SOURCE BADGE in the merged inbox. Quiet, not a status: a team upload is
   not better or worse than a demo somebody sent, it just arrived a different
   way, and the row still has to read as one line.

   RESTYLED 2026-10-01. It was a bordered, uppercase, letter-spaced box, which
   is the loudest treatment on the row — louder than the band, which is the one
   thing on the line that ranks anything. Uppercase also made two words wider
   than the three facts behind them. Now it is a filled token at the meta
   line's own size: present at a glance, and not the first thing the eye
   lands on. */
.ib-src { font-size: 11.5px; letter-spacing: 0; color: var(--text-3);
  background: var(--surface-2); border: none;
  border-radius: var(--r-chip); padding: 1px 7px; white-space: nowrap; }
/* The lead line for a send-only label (migration 092). It is not decoration:
   for a label with no fit band this line IS the row's answer, so it reads at
   the weight the band would have had. */
.ir-lead { color: var(--text-2); }
.ir-lead b { color: var(--text); }
.ir-near .linkish { font-size: inherit; color: var(--text-2); }

/* ---------------------------------------------------------------------------
   PHONES. Not a mobile redesign — the console is a desktop tool — but the
   things that must still WORK on a phone (Nikhil, 2026-09-17, from a train:
   "the site doesn't need to be fully mobile optimized it just has to work").

   THE ACCOUNT MENU WAS UNUSABLE. It is positioned against the account chip,
   and at 900px that became `right: 0` on the chip — fine while the chip sits
   near the right edge, and wrong the moment it does not. On a 390px screen
   with the chip about a third of the way across, a 230-290px menu anchored to
   the chip's right edge extends left THROUGH the edge of the screen: the
   account list, the artist-profile link and Log out were all half off-frame
   and the switcher could not be used at all.

   So below 560px it stops being a popover and becomes a sheet pinned to the
   VIEWPORT. position: fixed takes it out of the chip's coordinate space
   entirely, which is the only way its width can be guaranteed to fit.
   --------------------------------------------------------------------------- */
@media (max-width: 560px) {
  .account-menu {
    position: fixed;
    top: auto; bottom: 10px; left: 10px; right: 10px;
    min-width: 0; max-width: none;
    max-height: min(70vh, 460px); overflow-y: auto;
    border-radius: 12px;
  }
  /* Touch targets, on the two controls this sheet exists for. */
  .am-row, .am-item { padding-top: 10px; padding-bottom: 10px; }
  /* The workspace switcher beside it has the same anchoring problem. */
  .account-who { max-width: 100%; }
}


/* ===================== THE LABEL'S OWN PAGE =====================
   The shape every profile has settled on: a wide banner, a round picture
   overlapping its lower edge, identity to the right. Modelled on the pages a
   label already uses -- LinkedIn's bounded card, SoundCloud's overlap.

   THE CARD NEEDS ITS OWN EDGE, unlike every other .card here. The house rule is
   "no outline, the surface tone against the page ground is the card's whole
   edge, in both themes" -- but in LIGHT theme --surface and --bg are BOTH
   #ffffff, so that edge is zero contrast and the card dissolves into the page.
   An ordinary card survives that because its content is text in a column. This
   one is a full-bleed image with a circle hanging off it, and with no boundary
   it reads as debris on the page rather than as a card. Hence a real border and
   a larger radius than --r. */
.prof-card {
  padding: 0;
  overflow: hidden;
  border: 1px solid var(--border);
  border-radius: 16px;
  margin-bottom: 18px;
}

/* The banner. Taller than it was (Nikhil, 2026-10-02: "make the header a
   little taller"), and bounded top and bottom so it is a band on a wide
   monitor rather than a mural. aspect-ratio keeps the proportion the uploader
   previewed; the min/max stop it collapsing on a phone or dominating at
   1800px. */
.prof-banner {
  aspect-ratio: 4 / 1;
  min-height: 170px;
  max-height: 260px;
  background-color: var(--surface-2);
  background-size: cover;
  background-position: center;
  background-repeat: no-repeat;
}
/* No banner yet: a quiet wash rather than a flat grey slab, and one that works
   in both themes instead of a hardcoded dark gradient that looked like a
   mistake on a white page. */
.prof-banner:not(.has-image) {
  background-image:
    linear-gradient(120deg, rgba(112,101,240,0.22) 0%, rgba(112,101,240,0.06) 48%, rgba(216,161,58,0.14) 100%);
}

.prof-identity {
  display: flex; align-items: flex-end; gap: 18px;
  padding: 0 24px 16px; margin-top: -46px;
}

.prof-avatar {
  flex: 0 0 auto; width: 104px; height: 104px; border-radius: 50%;
  background: var(--surface-2) center/cover no-repeat;
  /* The ring is the page ground, so the circle reads as sitting ON the banner
     rather than being a hole cut through it. */
  box-shadow: 0 0 0 4px var(--surface);
  display: grid; place-items: center; overflow: hidden;
}
.prof-initials {
  font-size: 34px; font-weight: 600; color: var(--text-3);
  letter-spacing: -0.02em; user-select: none;
}
.prof-avatar.has-image .prof-initials { display: none; }

.prof-headline { flex: 1 1 auto; min-width: 0; padding-bottom: 6px; }
.prof-name {
  margin: 0; font-size: clamp(1.35rem, 2.6vw, 1.9rem); font-weight: 600;
  letter-spacing: -0.02em; line-height: 1.15; overflow-wrap: anywhere;
}
.prof-meta {
  margin: 6px 0 0; display: flex; gap: 10px; flex-wrap: wrap;
  color: var(--text-3); font-size: 13px;
}
.prof-handle { font-variant-numeric: tabular-nums; }
.prof-actions { flex: 0 0 auto; padding-bottom: 8px; }

.prof-bio {
  margin: 10px 24px 0; max-width: 62ch; color: var(--text-2);
  font-size: 14.5px; line-height: 1.6; white-space: pre-wrap;
}
.prof-genres { display: flex; flex-wrap: wrap; gap: 6px; margin: 14px 24px 0; }
.prof-genre {
  font-size: 12px; color: var(--text-2);
  border: 1px solid var(--border); border-radius: 999px; padding: 3px 10px;
}
.prof-links { display: flex; flex-wrap: wrap; gap: 8px; margin: 14px 24px 22px; }
.prof-link {
  display: inline-flex; align-items: center; gap: 7px;
  font-size: 13px; color: var(--text); text-decoration: none;
  border: 1px solid var(--border); border-radius: var(--r-ctl);
  padding: 7px 12px; background: var(--surface-2);
}
.prof-link:hover { border-color: var(--border-2); }
.prof-link svg { flex: 0 0 auto; opacity: 0.75; }
.prof-empty { color: var(--text-3); font-size: 14px; }

@media (max-width: 620px) {
  .prof-identity { flex-wrap: wrap; gap: 12px; margin-top: -36px; padding-left: 16px; padding-right: 16px; }
  .prof-avatar { width: 78px; height: 78px; }
  .prof-initials { font-size: 26px; }
  .prof-actions { width: 100%; padding-bottom: 0; }
  .prof-actions .btn { width: 100%; }
  .prof-bio, .prof-genres, .prof-links { margin-left: 16px; margin-right: 16px; }
  .prof-banner { min-height: 120px; }
}

/* ---------- the edit form ---------- */
.prof-edit { margin: 18px 24px 24px; padding-top: 18px; border-top: 1px solid var(--border); }
.prof-edit-images { display: flex; flex-wrap: wrap; gap: 24px; margin-bottom: 22px; }
.prof-pick { display: flex; flex-direction: column; gap: 9px; align-items: flex-start; }
.prof-pick-label, .prof-field-label {
  font-size: 12px; text-transform: uppercase; letter-spacing: .08em;
  color: var(--text-3); font-weight: 600;
}
.prof-pick .btn { cursor: pointer; }

/* THE PREVIEW BOX IS THE POINT (Nikhil, 2026-10-02: "a nice box so if its the
   wrong size the box will help them adjust it"). It is the destination's exact
   aspect ratio with the chosen file drawn in it under object-fit: cover -- so
   what the reader sees here is precisely what the page will show, including the
   crop. A file that is the wrong shape is not rejected; it is shown being
   cropped, which is the information somebody needs in order to go and fix it.
   The hint underneath names the size that needs no cropping at all. */
.prof-prev {
  position: relative; overflow: hidden;
  background: var(--surface-2);
  border: 1px dashed var(--border-2);
  display: grid; place-items: center;
}
.prof-prev img { width: 100%; height: 100%; object-fit: cover; display: block; }
.prof-prev-banner { width: 320px; max-width: 100%; aspect-ratio: 4 / 1; border-radius: 10px; }
.prof-prev-avatar { width: 104px; height: 104px; border-radius: 50%; }
/* The placeholder glyph, shown until a file is chosen. */
.prof-prev-hint {
  font-size: 11.5px; color: var(--text-3); text-align: center;
  padding: 0 10px; line-height: 1.45;
}
.prof-prev img + .prof-prev-hint { display: none; }
.prof-pick-note { font-size: 11.5px; color: var(--text-3); }
/* Said only when it matters: the file is smaller than the box it has to fill,
   so it will be upscaled and look soft. */
.prof-pick-warn { font-size: 11.5px; color: var(--gold, #d8a13a); }

.prof-field { display: flex; flex-direction: column; gap: 7px; margin-bottom: 14px; flex: 1 1 220px; }
.prof-field textarea {
  background: var(--surface-2); border: 1px solid var(--border); color: var(--text);
  border-radius: var(--r-ctl); padding: 10px 12px; font: 14px/1.55 var(--font);
  outline: none; width: 100%; resize: vertical; min-height: 88px;
}
.prof-field textarea:focus { border-color: var(--border-2); }
.prof-edit-row { display: flex; gap: 14px; flex-wrap: wrap; }
.prof-field-narrow { flex: 0 0 120px; }
.prof-field-narrow input { text-transform: uppercase; }

/* The genre picker. Chips in the SAME pill shape as the read-only .prof-genre
   row on the card above, so a pressed chip and the chip it produces are
   recognisably one thing — a picker that looks nothing like its own result is
   how somebody saves and then wonders what they changed.

   THE PRESSED STATE IS .chip-btn.active's, VERBATIM: a fully inverted chip,
   via --invert-bg/--invert-fg. Not a new accent colour, and not for tidiness —
   this console has a light theme (:root at line 57) and a hardcoded violet
   would be the one selected state in the app that does not flip with it. An
   inverted chip is also the only "on" treatment that needs no opinion about
   hue. A tick would be a second mark to read for the same fact. */
.prof-field-hint { text-transform: none; letter-spacing: 0; font-weight: 400; color: var(--text-3); }
.prof-edit-genres { display: flex; flex-wrap: wrap; gap: 7px; margin: 8px 0 18px; }
.prof-chip {
  font-size: 12.5px; line-height: 1;
  padding: 7px 12px;
  background: none;
  color: var(--text-2);
  border: 1px solid var(--border);
  border-radius: 999px;
  cursor: pointer;
  font-family: inherit;
}
.prof-chip:hover { border-color: var(--border-2); color: var(--text); }
.prof-chip.on {
  background: var(--invert-bg);
  color: var(--invert-fg);
  border-color: var(--invert-bg);
}

.prof-edit-links { display: grid; gap: 10px; margin: 6px 0 18px; }
.prof-edit-link { display: flex; align-items: center; gap: 10px; }
.prof-edit-link-kind { flex: 0 0 104px; font-size: 13px; color: var(--text-2); }
.prof-edit-link input { flex: 1 1 auto; }
.prof-edit-actions { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }

@media (max-width: 620px) {
  .prof-edit { margin-left: 16px; margin-right: 16px; }
  .prof-edit-link-kind { flex-basis: 84px; }
  .prof-prev-banner { width: 100%; }
}

/* ======================================================================
   THE DEVELOPERS SCREEN (2026-10-01). The label's own drop box: the settings
   on the left, the real widget in an iframe on the right, the snippets under
   both. Two columns while there is room, stacked when there is not — the
   preview is the half that cannot usefully shrink, so it keeps a fixed width
   and the settings give way. */
.dev-layout {
  display: grid; grid-template-columns: minmax(0, 1fr) 420px;
  gap: 24px; align-items: start;
}
.dev-config { display: flex; flex-direction: column; gap: 14px; }
.dev-field { display: flex; flex-direction: column; gap: 6px; }
.dev-field label { font-size: 12.5px; color: var(--muted); }
.dev-input {
  width: 100%; padding: 8px 10px; border: 1px solid var(--border-2);
  border-radius: var(--r-ctl); background: var(--surface); color: var(--text);
  font: inherit; font-size: 14px;
}
.dev-colour { display: flex; align-items: center; gap: 10px; }
/* A native colour well, sized to a control rather than to whatever the browser
   thinks a colour input is. The border is the same hairline every other field
   on this screen wears, so it reads as one form. */
.dev-colour input[type="color"] {
  width: 52px; height: 34px; padding: 2px; cursor: pointer;
  border: 1px solid var(--border-2); border-radius: var(--r-ctl);
  background: var(--surface);
}
.dev-preview { display: flex; flex-direction: column; gap: 8px; }
/* The frame carries the embed's own card, which draws its own background, so
   this is a well for it rather than a panel of its own. */
/* 420px is a STARTING height, replaced by the first `nine71:resize` the embed
   posts (developers.js listens for it, exactly as the loader script does). It
   is deliberately shorter than the snippet's 560: the first step of the widget
   is a dropzone and a line, and a frame taller than its content shows a band of
   the iframe's own blank canvas, which reads as a broken preview. */
#dev-frame {
  width: 100%; height: 420px; border: 1px solid var(--border);
  border-radius: var(--r-card, 12px); background: transparent;
  color-scheme: light dark;
}
.dev-snip { margin-top: 14px; }
.dev-snip-head {
  display: flex; align-items: center; justify-content: space-between;
  gap: 12px; margin-bottom: 6px;
}
/* The snippet is the thing being copied, so it is shown whole: wrapped, not
   scrolled sideways. A horizontal scrollbar over code someone is about to
   select by eye hides the half that matters. */
.dev-code {
  margin: 0; padding: 12px 14px; background: var(--surface-2);
  border: 1px solid var(--border); border-radius: var(--r-ctl);
  font-family: var(--mono, ui-monospace, SFMono-Regular, Menlo, monospace);
  font-size: 12.5px; line-height: 1.55; color: var(--text-2);
  white-space: pre-wrap; word-break: break-word;
}
@media (max-width: 1100px) {
  .dev-layout { grid-template-columns: minmax(0, 1fr); }
  .dev-preview { max-width: 560px; }
}

/* The organization-type setting (2026-10-02). A segmented control and its Save
   on one line, wrapping to two on a narrow window rather than squeezing the
   three words inside the buttons. */
.org-kind-row {
  display: flex; align-items: center; gap: 10px; flex-wrap: wrap;
  margin: 10px 0 6px;
}
