/* ============================================================
   Elisonic — the application stylesheet (app.css).
   Today this is the "Dispatch" design system Warehouse Radio was
   built in: a rack front panel in black matte anodise, with
   printed labels applied to it. Safety-yellow markings, hazard
   stripe, freight-manifest cards. WCAG AA throughout.
   A station theme lists its own stylesheets in theme.toml; the
   Elisonic identity for the application's own pages arrives with
   the app shell (plan §6), when the station-front sections here
   move into the Warehouse Radio theme.
   ============================================================ */

/* ═══════════════════════════════════════════════════════════════════
   BLACK MATTE ANODISE.

   One material: a rack front panel, brushed horizontally the way the
   extrusion runs. The only bright things on it are the LABELS — the
   barcode and the dithered cover — which are printed stock applied to the
   panel rather than surfaces the interface is built from.

   MATTE IS THE CONSTRAINT THAT MAKES IT READABLE. A matte surface has
   almost no specular lobe, so there is no sheen sweep, no diagonal gloss,
   no sparkle. Every one of those turns a dark panel into a dark mirror,
   and a dark mirror is where legibility goes. What is left is a faint
   directional grain and light that only touches edges.

   ONE THEME, and that is the premise rather than a shortcut: an enclosure
   that changes colour with the time of day is not an enclosure. The light
   theme this replaces is removed rather than left half-maintained.
   ═══════════════════════════════════════════════════════════════════ */

:root {
  /* ---- the chassis ---- */
  --ano-950:   #060709;
  --ano-900:   #0A0B0D;
  --ano-800:   #0F1013;
  --ano-700:   #141519;
  --ano-600:   #191B20;
  --ano-500:   #22242A;
  /* One key light, cool. One ambient, warm. Raw channels, so the edge
     gradients can vary alpha alone. */
  --hi:        214, 228, 255;
  --lo:        8, 6, 4;

  /* ---- surfaces, under the names this file already uses ----
     Kept on purpose: ~180 declarations reference these, and remapping the
     VALUES carries the whole stylesheet across without touching rules that
     were never about colour in the first place. */
  --paper:      var(--ano-900);   /* the ground */
  --paper-2:    var(--ano-600);   /* a raised strip */
  --card:       #0C0D10;          /* a pocket milled into the panel */
  --ink:        #E6E4DE;          /* 14.4:1 on --ano-700 */
  --ink-2:      #353B45;          /* metal one grade up: control hover */
  --dim:        #A7A399;          /*  7.3:1 */
  --line:       rgba(255,255,255,.07);

  /* ---- marking vs lamp ----
     Yellow is PRINTED onto the metal: hazard stripe, badge, active tab.
     Amber is EMITTED and belongs to on-air alone. A lamp that also
     decorates is a lamp nobody reads. */
  --yellow:      #F2C81E;
  --yellow-deep: #DCB412;
  --on-yellow:   #060709;
  --lamp:        #FFAE3C;
  --lamp-core:   #FFD9A0;

  /* The emphasis control. On paper this was the darkest thing on the page
     carrying yellow type; on metal it is a machined key one grade lighter
     than the panel it sits on. */
  --invert-bg:  var(--ano-500);
  --invert-fg:  #E6E4DE;
  --bar-accent: #F2C81E;

  /* ---- signals ----
     Saturated grounds carry white text; the -text variants are the same
     signals set ON the panel, where they must be lighter to be read. One
     value cannot do both jobs. */
  --red:       #C0392B;   /* badge ground, white text 5.0:1 */
  --ok:        #1F7A44;   /* badge ground, white text 4.9:1 */
  --red-text:  #FF8175;
  --ok-text:   #59D183;
  --warn-text: #DCC24E;

  /* ---- printed labels ----
     Light stock, always. A Code 128 symbol is dark bars on a light
     substrate — that is what the symbol IS — and the dithered cover is the
     same ink on the same paper. These never invert with a theme. */
  --label-bg:     #FBFAF6;
  --label-ink:    #141209;
  --wave-played:  #141209;
  --wave-pending: #141209;

  --placeholder: #6E6A60;
  --focus:       #7FB0FF;

  /* ---- texture ----
     Thresholded on RGB with alpha held at 1, so each tile is an opaque
     greyscale field rather than sparse transparent specks. That is what
     lets them composite through `overlay`, which MODULATES the surface
     beneath — a white-flooded layer at plain opacity can only lift
     everything toward grey, which is how texture eats contrast.
     stitchTiles='stitch' is what stops a seam at every tile boundary. */
  --tex-brush: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='300' height='200'%3E%3Cfilter id='r'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.004 1.4' numOctaves='2' seed='4' stitchTiles='stitch'/%3E%3CfeColorMatrix type='saturate' values='0'/%3E%3C/filter%3E%3Crect width='300' height='200' filter='url(%23r)'/%3E%3C/svg%3E");
  --tex-grain: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='180' height='180'%3E%3Cfilter id='g'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='2' seed='7' stitchTiles='stitch'/%3E%3CfeColorMatrix type='saturate' values='0'/%3E%3C/filter%3E%3Crect width='180' height='180' filter='url(%23g)'/%3E%3C/svg%3E");

  /* Geometry */
  --tap:        52px;
  --tap-min:    44px;
  --header-h:   64px;
  --radius:     3px;       /* near-square; softness is not the mood */
  --edge:       1px;       /* hairlines, not the old 2px freight border */
  --s1: 0.25rem; --s2: 0.5rem; --s3: 0.75rem; --s4: 1rem; --s5: 1.5rem; --s6: 2rem; --s7: 3rem;

  /* Elevation is OCCLUSION — the contact shadow a raised thing casts —
     never a brighter outline. Panel and ground differ by 1.08:1, so the
     shadow is what separates them; a lighter edge is a border with extra
     steps. */
  --shadow: 0 0 0 1px rgba(var(--lo),.55), 0 1px 3px rgba(var(--lo),.5),
            0 10px 26px -14px rgba(var(--lo),.7), inset 0 1px 0 rgba(var(--hi),.035);
  --ease: cubic-bezier(.2,.6,.2,1);

  --gutter:  max(18px, env(safe-area-inset-left));
  --gutter-r: max(18px, env(safe-area-inset-right));

  /* Type. Big Shoulders sets the wordmark and the now-playing title and
     nothing else; every interface label is Archivo, because a condensed
     face at 12px with small-caps tracking reads cramped and airy at once.
     Space Mono carries data, where it reads as machine output. */
  --mark-url:   url(/favicon.svg);   /* the brand mark, masked; a theme sets its own */
  --display:    'Big Shoulders', 'Arial Narrow', system-ui, sans-serif;
  --label-face: 'Archivo', system-ui, -apple-system, sans-serif;
  --body:       'Archivo', system-ui, -apple-system, sans-serif;
  --mono:       'Space Mono', ui-monospace, 'SF Mono', Menlo, monospace;
}


* { box-sizing: border-box; }

/* Tells the browser to render ITS OWN chrome — scrollbars, native form
   controls, the overscroll canvas — dark. Without it those stay light and
   punch holes through the panel. */
html { color-scheme: dark; }

html {
  -webkit-text-size-adjust: 100%;
  scroll-behavior: smooth;
}
html, body {
  margin: 0;
  background: var(--paper);
  color: var(--ink);
  font-family: var(--body);
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
}
body { min-height: 100dvh; overflow-x: hidden; display: flex; flex-direction: column; }
/* Atmospheric grain — what sits between the viewer and the room, as opposed
   to the finish on any one surface. Kept faint, and soft-light so it never
   lifts black toward grey. Fixed, so it does not slide under a scroll. */
body::after {
  content: ""; position: fixed; inset: 0; z-index: 60; pointer-events: none;
  opacity: .07; mix-blend-mode: soft-light;
  background-image: var(--tex-grain); background-size: 180px 180px;
}

::selection { background: var(--yellow); color: var(--on-yellow); }
* { -webkit-tap-highlight-color: transparent; }
@media (prefers-reduced-motion: reduce) { html { scroll-behavior: auto; } }

/* Scrollbars — thin, on-brand (Firefox + WebKit). The thumb's border matches
   the surface so it reads as an inset pill with breathing room. */
* { scrollbar-width: thin; scrollbar-color: var(--dim) transparent; }
::-webkit-scrollbar { width: 12px; height: 12px; }
::-webkit-scrollbar-track { background: transparent; }
::-webkit-scrollbar-thumb {
  background: var(--dim); border: 3px solid var(--paper);
  border-radius: 999px; background-clip: padding-box;
}
::-webkit-scrollbar-thumb:hover { background: var(--ink); }
::-webkit-scrollbar-corner { background: transparent; }
.app-side::-webkit-scrollbar-thumb,
.table-wrap::-webkit-scrollbar-thumb,
.import-box::-webkit-scrollbar-thumb { border-color: var(--card); }

/* Shared motifs -------------------------------------------------- */

.hazard {
  height: 7px;
  background: repeating-linear-gradient(-45deg,
    var(--yellow) 0 11px, var(--ano-950) 11px 22px);
  opacity: .9;
}

/* 800 exactly. Only one weight of the display face is shipped, and asking for
   700 or 900 makes the browser SYNTHESISE it — a smeared or thinned fake of a
   drawn weight, which is worse than the weight you did not want. */
.brand, .brand-sm {
  font-family: var(--display);
  font-weight: 800;
  text-transform: uppercase;
  letter-spacing: .01em;
  line-height: .88;
  margin: 0;
}
/* Fluid so the condensed display face can't overrun a 320px phone — and, more
   importantly, so it still fits if the webfont fails and we fall back to a
   non-condensed system face. */
.brand { font-size: clamp(2.25rem, 11vw, 2.75rem); }
.brand-sm {
  display: flex; align-items: center; gap: 10px;
  font-family: var(--display); font-weight: 800; text-transform: uppercase;
  font-size: clamp(1.25rem, 5vw, 1.7rem); line-height: .92; letter-spacing: .012em;
  min-width: 0; color: #F2F0EA;
  /* It is a link in every header now — the logo goes home, which is the
     one navigation people try without being told. */
  text-decoration: none;
}
a.brand-sm:focus-visible { outline: 2px solid var(--lamp); outline-offset: 4px; border-radius: 2px; }
/* MASKED, NOT AN <img>. An SVG loaded through <img> is an isolated document:
   currentColor inside it resolves against ITS root, not the page, so the mark
   would paint black on a black header. As a mask the shape comes from the file
   and the ink comes from here.

   ONE DRAWING EVERYWHERE. An earlier pass gave the header the simplified cut
   on the grounds that a lattice brace lands on a third of a pixel at ~36px.
   That is true and it was still the wrong call: the mark is the mark, and a
   header showing a different drawing from the one on the gate is two logos.
   The simplified cut stays where it is genuinely forced — the favicon, at
   16px, where the browser gives us no say. */
.mark, .mark-lg {
  flex: none; display: inline-block; background-color: currentColor;
  -webkit-mask-repeat: no-repeat; mask-repeat: no-repeat;
  -webkit-mask-position: center;  mask-position: center;
  -webkit-mask-size: contain;     mask-size: contain;
}
/* em, so it tracks the wordmark instead of drifting from it at a breakpoint. */
.mark {
  /* 1.9em put the mark taller than the cap height of the wordmark beside it,
     which reads as a badge rather than as a lockup. 1.35 sits it inside the
     line. */
  width: 1.35em; aspect-ratio: 423 / 414;
  -webkit-mask-image: var(--mark-url); mask-image: var(--mark-url);
}
.mark-lg {
  display: block; width: clamp(72px, 24vw, 104px); aspect-ratio: 423 / 414; margin: 0 0 14px;
  -webkit-mask-image: var(--mark-url); mask-image: var(--mark-url);
}

.mono { font-family: var(--mono); font-variant-numeric: tabular-nums; }

/* "Coming up" is a live channel, not a fault. It was drawing in --red with a
   red halo, which on a dark panel reads as an alarm sitting permanently next
   to a heading — and an alarm that is always on is one nobody reads. It is the
   same amber lamp the manifest head and the play button use. */
.live-dot {
  display: inline-block; width: 8px; height: 8px; border-radius: 50%;
  background: var(--lamp-core);
  box-shadow: 0 0 0 1px rgba(var(--lo),.5), 0 0 5px 0 var(--lamp),
              0 0 14px 2px color-mix(in srgb, var(--lamp) 45%, transparent);
}

/* Buttons -------------------------------------------------------- */

/* A key on a pack bench is struck a thousand times a day by somebody who is
   not looking at it, so the feedback is quick rather than large: one pixel
   down and most of the elevation gone.

   METAL, NOT YELLOW. These were a full saturated yellow ground, which made
   every button the loudest object on the screen and put the marking colour on
   a control. Yellow marks the chassis — the stripe, the badge, the active tab
   — and amber is the on-air lamp. A control is neither. */
.btn-go, .go, .add, .rm, .btn-ctl {
  /* ONE BOX MODEL FOR <button> AND <a>. A <button> centres its label
     vertically on its own; an <a> wearing the same class and a min-height
     does not — its text sat at the top of the box (the admin overview's
     download links). Flex centring makes the element irrelevant, and the
     explicit line-height stops a button (UA `normal`) and an anchor
     (inherited) from getting different heights for the same text. */
  display: inline-flex; align-items: center; justify-content: center;
  line-height: 1.2; text-decoration: none;
  font-family: var(--label-face);
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: .11em;
  cursor: pointer;
  color: var(--ink);
  background-color: var(--ano-500);
  background-image: var(--tex-brush), linear-gradient(180deg, rgba(var(--hi),.09), rgba(var(--lo),.22));
  background-size: 300px 200px, auto;
  background-blend-mode: overlay, normal;
  text-shadow: 0 -1px 0 rgba(var(--lo),.6);
  border: 0;
  border-radius: 2px;
  box-shadow: inset 0 1px 0 rgba(var(--hi),.16), 0 1px 2px rgba(var(--lo),.5),
              0 6px 16px -8px rgba(var(--lo),.6);
  transition: transform 90ms var(--ease), box-shadow 90ms var(--ease), background-color .12s linear;
}
@media (hover: hover) {
  .btn-go:hover, .go:hover, .add:hover, .rm:hover, .btn-ctl:hover { background-color: var(--ink-2); }
}
.btn-go:active, .go:active, .add:active, .rm:active, .btn-ctl:active {
  transform: translateY(1px);
  box-shadow: inset 0 1px 0 rgba(var(--hi),.08), inset 0 2px 6px rgba(var(--lo),.4),
              0 1px 2px rgba(var(--lo),.5);
}

.btn-go {                       /* full-width primary (login) */
  width: 100%; min-height: var(--tap);
  font-size: 1rem;
}
.btn-go:disabled { opacity: .55; cursor: default; }

.link-btn {
  /* Both <a> and <button> wear this, side by side in the top bar. Without an
     explicit line-height the button (UA `normal`) and the anchor (inherited)
     get different boxes for the same text, and the underlines land at
     different heights. Same fix as the buttons above: one declared box. */
  display: inline-flex; align-items: center;
  position: relative;
  background: none; border: 0; color: var(--dim);
  font-family: var(--mono); font-size: 0.75rem; line-height: 1.2; letter-spacing: .02em;
  cursor: pointer; padding: 8px;
  /* The underline is drawn by text-decoration so it follows wrapped text (the
     gate's sentence-length links), but its geometry is pinned: a hairline,
     pushed clear of the mono face's descenders, in a quieter tone than the
     text so the words read first. Hover brings it up to the chassis yellow. */
  text-decoration: underline;
  text-decoration-thickness: 1px;
  text-underline-offset: 0.32em;
  text-decoration-color: color-mix(in srgb, currentColor 40%, transparent);
  transition: color .12s linear, text-decoration-color .12s linear;
}
/* These small mono links are the only nav on every page, so they have to be
   properly tappable — but growing them to 44px would bloat the header. Expand
   the hit area vertically instead; the links are separated horizontally, so the
   invisible targets can't overlap each other. */
.link-btn::after {
  content: ""; position: absolute; left: 0; right: 0; top: 50%;
  height: var(--tap-min); transform: translateY(-50%);
}
@media (hover: hover) {
  .link-btn:hover { color: var(--ink); text-decoration-color: var(--yellow); text-decoration-thickness: 2px; }
}
.link-btn:focus-visible { text-decoration-color: var(--yellow); }

/* Anything with the hidden attribute stays hidden, whatever display its class
   declares — the passkey link in the gate is a .link-btn toggled this way, and
   an author `display` would otherwise override the UA's [hidden] rule. */
[hidden] { display: none !important; }

/* Accessibility floors ------------------------------------------ */

:focus-visible { outline: 3px solid var(--focus); outline-offset: 2px; }
button:focus-visible, .go:focus-visible, .add:focus-visible { outline-offset: 3px; }
@media (prefers-reduced-motion: reduce) {
  * { animation: none !important; transition: none !important; }
}

/* Safari on iOS zooms the whole viewport when you focus a field whose text is
   under 16px — and never zooms back out. Several admin controls are deliberately
   compact (0.6875–0.875rem), so lift them to 16px on touch devices only; pointer
   devices keep the tighter type. */
@media (pointer: coarse) {
  input, select, textarea { font-size: 1rem; }
}

/* ============================================================
   Gate (login)
   ============================================================ */

.gate { min-height: 100dvh; display: flex; flex-direction: column; }
.gate .hazard { flex: none; }
.gate-card {
  width: 100%; max-width: 420px;
  margin: auto; padding: 40px 26px;
}
.gate .brand { font-size: clamp(2.5rem, 13vw, 3.25rem); margin-bottom: 6px; }
.gate-sub { color: var(--dim); font-size: 0.9375rem; margin: 0 0 30px; }

.field { display: block; margin-bottom: 18px; }
.field-l {
  display: block;
  font-family: var(--label-face); font-weight: 600; text-transform: uppercase;
  letter-spacing: .13em; font-size: 0.6875rem; margin-bottom: 8px; color: var(--ink);
}
.field-l .hint { font-family: var(--mono); font-weight: 400; text-transform: none; letter-spacing: 0; font-size: 0.6875rem; color: var(--dim); }

/* A pocket milled into the panel: shadow at the top where the wall cuts the
   light, a hairline of catch along the bottom where it comes back. The 2px
   light outline this replaces was the thing that read as DRAWN — a recess is
   defined by occlusion, not by a border around it. */
input {
  width: 100%; min-height: var(--tap);
  background: var(--card); color: var(--ink);
  border: 0; border-radius: 2px;
  box-shadow: inset 0 2px 7px rgba(var(--lo),.75), inset 0 -1px 0 rgba(var(--hi),.035);
  padding: 0 14px; font-family: var(--body); font-size: 1rem;
}
input::placeholder { color: var(--placeholder); }

.error {
  font-family: var(--mono); font-size: 0.8125rem; color: #fff;
  background: var(--red); border-radius: var(--radius);
  padding: 9px 12px; margin: 0 0 16px;
}

/* ============================================================
   App shell
   ============================================================ */

/* Sticky header keeps the brand + status pinned on long scrolls. */
.siteheader {
  position: relative;
  position: sticky; top: 0; z-index: 20;
  background-color: var(--ano-700);
  background-image:
    linear-gradient(to bottom, rgba(var(--hi),.028), transparent 18px),
    linear-gradient(180deg, rgba(var(--hi),.008), transparent 30%, rgba(var(--lo),.12));
  border-bottom: 1px solid rgba(var(--lo),.7);
}
/* Brushed, like the panels. No backdrop-filter: a blur behind a sticky header
   is a per-frame composite on every scroll, and on an opaque metal bar there
   is nothing showing through for it to blur. */
.siteheader::before {
  content: ""; position: absolute; inset: 0; pointer-events: none; z-index: 0;
  opacity: .12; mix-blend-mode: overlay;
  background-image: var(--tex-brush); background-size: 300px 200px;
}
.siteheader > * { position: relative; z-index: 1; }
.siteheader .hazard { height: 10px; }
.topbar {
  display: flex; align-items: center; justify-content: space-between; gap: var(--s3);
  padding: 12px var(--gutter-r) 12px var(--gutter);
  padding-top: max(12px, env(safe-area-inset-top));
  flex-wrap: wrap;      /* brand + 3 links can exceed a 320px phone; wrap, don't clip */
}
.topbar-right { display: flex; align-items: center; gap: var(--s3); flex-wrap: wrap; justify-content: flex-end; }
/* The signed-in name, first in the header's right-hand cluster. Same mono face
   and padding as the links beside it so it sits on their baseline, but ink
   rather than dim and with no underline: it is a fact, not an action, and the
   underline is what marks the actions. The ADMIN tag rides along on the app
   pages; on the admin itself the brand carries it. */
.who {
  display: inline-flex; align-items: center; gap: 6px;
  padding: 8px 8px 8px 0;
  font-family: var(--mono); font-size: 0.75rem; line-height: 1.2; letter-spacing: .02em;
  color: var(--ink); font-weight: 700;
}
.who .admin-tag { vertical-align: 0; margin-left: 0; }

.stack {
  max-width: 620px; margin: 0 auto;
  padding: var(--s3) var(--gutter-r) calc(var(--s7) + env(safe-area-inset-bottom)) var(--gutter);
}

/* ============================================================
   Manifest (now playing) + waveform
   ============================================================ */

.manifest {
  position: relative;
  background-color: var(--ano-700);
  background-image:
    linear-gradient(to bottom, rgba(var(--hi),.026), transparent 16px),
    linear-gradient(180deg, rgba(var(--hi),.007), transparent 30%, rgba(var(--lo),.10));
  border-radius: var(--radius);
  box-shadow: var(--shadow);
  overflow: hidden;
}
/* The brushing. `overlay` at 12% MODULATES the panel — lightening and
   darkening around its own value — where a white layer at plain opacity can
   only lift everything toward grey. That difference is the entire reason
   text stays readable on a textured surface. */
.manifest::before {
  content: ""; position: absolute; inset: 0; pointer-events: none; z-index: 0;
  opacity: .12; mix-blend-mode: overlay;
  background-image: var(--tex-brush); background-size: 300px 200px;
}
.manifest > * { position: relative; z-index: 1; }
/* The head was a full-width slab of saturated yellow. It is a dark strip with
   a lamp on it now: the accent went from being the loudest thing on the card
   to being the only thing that emits. */
.mhead {
  display: flex; align-items: center; justify-content: space-between;
  background: var(--ano-600); color: var(--ink);
  border-bottom: 1px solid rgba(var(--lo),.6);
  box-shadow: inset 0 1px 0 rgba(var(--hi),.03);
  padding: 9px 14px;
}
.mhead .l {
  display: flex; align-items: center; gap: 9px;
  font-family: var(--label-face); font-weight: 600; text-transform: uppercase;
  letter-spacing: .15em; font-size: 0.6875rem;
}
/* THE LAMP. Emitted light: a core, a tight halo, a wide bloom. The only
   glowing thing in the interface, which is what makes it worth looking at. */
.mhead .l::before {
  content: ""; width: 8px; height: 8px; border-radius: 50%; flex: none;
  background: var(--lamp-core);
  box-shadow: 0 0 0 1px rgba(var(--lo),.5), 0 0 5px 0 var(--lamp),
              0 0 14px 2px color-mix(in srgb, var(--lamp) 45%, transparent);
}
.manifest[data-state="idle"] .mhead .l::before,
.manifest[data-state="offair"] .mhead .l::before {
  background: #3A3D44; box-shadow: 0 0 0 1px rgba(var(--lo),.5);
}
.mhead .n { font-family: var(--mono); font-size: 0.8125rem; color: var(--dim); font-variant-numeric: tabular-nums; }
/* Two columns: the cover fills the row's height on the left, everything else
   stacks on the right. */
.mbody { padding: 16px 14px 14px; display: flex; gap: 14px; align-items: stretch; }
.mstack {
  flex: 1 1 auto; min-width: 0;
  display: flex; flex-direction: column; gap: 12px;
  /* Distribute over whatever height the cover sets, so the consignment line
     lands on the cover's bottom edge instead of floating above it. */
  justify-content: space-between;
}
/* .mmeta takes the slack, so the barcode and consignment line sit against the
   bottom edge — which is what makes the cover's height look intended rather
   than coincidental. */
.mbody .t {
  font-family: var(--display); font-weight: 800; text-transform: uppercase;
  font-size: clamp(1.9rem, 8vw, 2.6rem); line-height: .9; margin: 0;
  letter-spacing: .004em; text-wrap: balance;
}
.mbody .a { font-weight: 600; font-size: 0.9375rem; color: var(--dim); margin: 5px 0 0; }
/* "Packed by <name>" + the full tidal.com/track/<id> tag. Together these exceed
   a 320px card, and .manifest clips overflow — so wrap, and ellipsis each half
   rather than letting either be silently cut off mid-word. */
.consign {
  display: flex; justify-content: space-between; gap: 4px 10px; flex-wrap: wrap;
  font-family: var(--mono); font-size: 0.6875rem; color: var(--dim);
  border-top: 1px dashed var(--line); padding-top: 10px; margin-top: 4px;
}
.consign > span { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.consign b, #np-consign b { color: var(--ink); }

/* Visualiser — a real Code 128 barcode of tidal.com/track/<id> rendered as a
   waveform: each bar keeps the barcode's true width (--w) but gets a waveform
   height (--h) and a "dance" delay (--d); spaces are transparent gaps. The
   played portion (progress) is ink; the rest is grey. */
.wave {
  /* align-items: STRETCH (the default), deliberately. The bars used to be
     bottom-aligned with a percentage height, and WebKit does not resolve a
     percentage height on a flex item whose cross size comes from alignment
     rather than from a definite value — on iOS the barcode came out the wrong
     height while Chrome resolved it against the container and looked fine.
     Stretching gives every bar a definite height with no percentage involved,
     and the bar itself is then PAINTED to --h with a hard-stop gradient, whose
     percentage resolves against the element's own box and is not engine
     -dependent. */
  position: relative;
  display: flex; gap: 0; flex: none;
  height: 56px; padding: 7px 9px 6px;
  /* A printed label applied to the panel — light stock, because a Code 128
     symbol IS dark bars on a light substrate. It does not invert with
     anything; there is nothing for it to invert with. */
  background: var(--label-bg); border-radius: 2px;
  box-shadow: 0 0 0 1px rgba(var(--lo),.6), 0 1px 3px rgba(var(--lo),.55);
  overflow: hidden;   /* the dance must not print outside the label */
}
/* ONE INK, ONE HEIGHT. A Code 128 symbol carries its data in the alternating
   widths of bars AND spaces; every bar is the same height, and that uniformity
   is the whole visual cue that says "barcode". The bars stretch to fill the
   label, so there is no percentage height for an engine to disagree about and
   no gradient to paint one. */
.wave i {
  flex: var(--w, 1) 0 0; min-width: 0;
  background: var(--label-ink);
}
.wave i.gap { background: none; }                     /* barcode space */
/* Progress is a scan head. The wash it drags tints the SUBSTRATE — never the
   ink — so the symbol stays one colour the whole way across. */
.wave::before {
  content: ""; position: absolute; inset: 0 auto 0 0; pointer-events: none;
  width: calc(var(--progress, 0) * 100%); background: rgba(var(--lo),.07);
}
.wave::after {
  content: ""; position: absolute; top: 0; bottom: 0; pointer-events: none;
  left: calc(var(--progress, 0) * 100%); width: 2px; background: var(--lamp);
  box-shadow: 0 0 8px 1px color-mix(in srgb, var(--lamp) 60%, transparent);
}
.manifest[data-state="idle"] .wave::after,
.manifest[data-state="offair"] .wave::after { display: none; }
/* Nothing dances on its own. The bars move only when there is real audio to
   move them — see .is-live below — and the scan head carries the time. */

/* ── The live spectrum ─────────────────────────────────────────────────────
   While the stream is playing, the printed label becomes an analyser: the bar
   WIDTHS stay the Code 128 encoding, so the symbol still identifies the track,
   and only the HEIGHTS come from the FFT. Dropping .is-live restores the
   printed barcode with no state to unwind.

   This is what the fake CSS "dance" was imitating badly. The dance was wrong
   because it was invented; the spectrum is right because it is the audio. */
.wave.is-live i:not(.gap) {
  /* Bars are solid and stretched by default, so an explicit height would pin
     them to the TOP of the label. Anchor them to the baseline instead. */
  align-self: flex-end;
  height: var(--live, 2px);
  position: relative;
  /* No transition: listen.js already applies attack/release smoothing, and a
     CSS transition restarted every frame only adds lag and compositor work. */
}
/* The floating peak cap that holds, then drifts down. */
.wave.is-live i:not(.gap)::after {
  content: ""; position: absolute; left: 0; right: 0;
  bottom: var(--cap, 0px); height: 2px;
  /* --label-ink, NOT --ink. The label is white stock; --ink is the panel's
     near-white text colour, which drew the caps invisible once the palette
     moved to black matte. */
  background: var(--label-ink); opacity: .5;
}
@media (prefers-reduced-motion: reduce) {
  .wave.is-live i:not(.gap)::after { display: none; }
}
/* Idle / off-air: a flat dead-signal line. align-self overrides the stretch
   that gives the bars their height, and 2px is a definite length — so no
   percentage is involved and every engine agrees. */
.manifest[data-state="idle"] .wave i:not(.gap),
.manifest[data-state="offair"] .wave i:not(.gap) { align-self: center; height: 2px; }

/* Off air: mute the yellow head so it reads as "not live". */
.manifest[data-state="offair"] .mhead { background: var(--paper-2); color: var(--dim); }
.manifest[data-state="offair"] .mbody .t { color: var(--dim); }

.locknote {
  font-family: var(--mono); font-size: 0.75rem; color: var(--dim);
  margin: 12px 2px 0; display: flex; gap: 8px; align-items: flex-start;
}
.locknote::before { content: "⏻"; color: var(--ink); font-size: 0.875rem; line-height: 1.2; }

.boot { padding: 44px 22px; font-family: var(--mono); color: var(--dim); font-size: 0.8125rem; }

/* --- Two-column layout: main + queue sidebar --- */
.app-grid {
  display: grid; grid-template-columns: 1fr; gap: var(--s5);
  grid-template-areas: "main" "side" "feed";
  max-width: 620px; margin: 0 auto;
  padding: var(--s5) var(--gutter-r) calc(var(--s7) + env(safe-area-inset-bottom)) var(--gutter);
}
.app-main { grid-area: main; }
.app-side { grid-area: side; }
.app-feed { grid-area: feed; }
/* Grid and flex items default to min-width:auto — they refuse to shrink below
   their own content. A queue title is white-space:nowrap, so its min-content is
   the whole untruncated string; that measurement climbs the tree and sets the
   grid column, and one 60-character track name made the document 802px wide
   inside a 390px phone. The ellipsis further down never got a chance: you
   cannot truncate against a box that grew to fit you.
   State it once for every region rather than patching whichever child is
   currently overflowing — .app-main alone used to carry this, and .lp (the
   listen page, further down) had the same hole for the same reason. */
.app-grid > *,
.lp > * { min-width: 0; }
.app-side { display: flex; flex-direction: column; gap: var(--s4); }

/* Tablet step (iPad portrait, split-screen, small laptop windows). Still one
   column — two would squeeze both — but the column stops being a narrow ribbon
   marooned in empty margins. .stack (admin) gets the same room. */
@media (min-width: 43.75em) {
  .app-grid, .stack { max-width: 760px; }
}

@media (min-width: 56.25em) {
  .app-grid {
    grid-template-columns: minmax(0, 1fr) 340px;
    /* The queue sits beside the player, where it is read against what is on
       air. The feed drops under the main column instead of stacking below the
       queue, which is what made one column twice the height of the other. */
    grid-template-areas: "main side" "feed side";
    grid-template-rows: auto 1fr;
    max-width: 1060px; gap: var(--s6); align-items: start;
  }
  /* Admin is the data-dense surface — tables, library cards, settings — so it
     gets real width here instead of scrolling inside a 620px ribbon. */
  .stack { max-width: 1100px; }
  /* The sidebar is a column, not a panel with its own scrollbar.
     It used to stick below the header with a max-height and overflow-y:auto,
     which produced a nested scroll region that STILL ran past the bottom of
     the window — it starts below the header, so a full-viewport height cannot
     fit. Two scrollbars to read one page, and the fix for the overflow made
     the overflow. The page scrolls once now; the column just ends. */
}

.side-block {
  background: var(--card); border: var(--edge) solid var(--line);
  border-radius: var(--radius); padding: 14px 16px;
  box-shadow: var(--shadow);
}
.side-block > h2 {
  display: flex; align-items: center; gap: 9px;
  font-family: var(--display); font-weight: 800; text-transform: uppercase;
  letter-spacing: .05em; font-size: 1rem; margin: 0 0 10px;
  padding-bottom: 9px; border-bottom: var(--edge) solid var(--line);
}
.side-note { font-family: var(--mono); font-size: 0.625rem; color: var(--dim); margin: 10px 0 0; line-height: 1.4; }

/* Coming-up queue (numbering encodes real position in the rotation) */
.queue { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; }
.qrow { display: flex; align-items: center; gap: 11px; padding: 9px 0; border-bottom: 1px solid var(--line); }
.qrow:last-child { border-bottom: 0; }
/* Only the first row carries a marker now. The queue used to number every slot,
   which promised a sequence the rotation never agreed to — slot 1 is a fact
   (already handed to the player) and everything behind it is a forecast that
   shifts as songs are added. NEXT says the true part; nothing says the rest. */
.qno {
  flex: none; align-self: flex-start; text-align: center;
  font-weight: 700; letter-spacing: .06em; font-size: 0.5625rem;
  padding: 3px 6px; border-radius: 2px;
  background: var(--yellow); color: var(--on-yellow);
}
.qrow.is-next .qt { font-weight: 800; }
.qrow .m { flex: 1; min-width: 0; }
.qt {
  font-family: var(--label-face); font-weight: 600; text-transform: uppercase; font-size: 1rem; line-height: 1;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.qs { font-family: var(--mono); font-size: 0.6875rem; color: var(--dim); margin-top: 3px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }

/* ============================================================
   Search
   ============================================================ */

.search { margin-top: var(--s6); }
.search .l {
  display: flex; align-items: baseline; gap: var(--s2);
  font-family: var(--display); font-weight: 800; text-transform: uppercase;
  letter-spacing: .05em; font-size: 1rem; margin-bottom: var(--s2);
}
.search-ind {
  font-family: var(--mono); font-size: 0.6875rem; color: var(--dim);
  text-transform: none; letter-spacing: 0; opacity: 0; transition: opacity .15s var(--ease);
}
.search-ind.htmx-request { opacity: 1; }
.searchrow {
  display: flex; border: var(--edge) solid var(--line);
  border-radius: var(--radius); overflow: hidden;
  transition: box-shadow .12s var(--ease);
}
.searchrow:focus-within { box-shadow: 0 0 0 3px rgba(10,88,255,.35); }
.searchrow .q {
  flex: 1; min-width: 0; border: 0; border-radius: 0;
  height: var(--tap); background: var(--card);
}
.searchrow .q:focus-visible { outline: none; }   /* the group shows focus */
.go {
  flex: none; height: var(--tap); padding: 0 18px;
  border-radius: 0 2px 2px 0; font-size: 0.875rem;
}

.results { margin-top: 12px; display: flex; flex-direction: column; gap: 10px; }
.results:empty { margin-top: 0; }

/* ============================================================
   Freight-label track rows (results + floor)
   ============================================================ */

.label {
  display: flex; align-items: center; gap: 12px;
  background: var(--card); border: var(--edge) solid var(--line);
  border-radius: var(--radius); padding: 11px 12px;
  transition: box-shadow .12s var(--ease), transform .12s var(--ease);
}
@media (hover: hover) {
  .label:hover { box-shadow: var(--shadow); transform: translateY(-1px); }
}
.label .m { flex: 1; min-width: 0; }
.label .t {
  font-family: var(--label-face); font-weight: 600; text-transform: uppercase;
  font-size: 1.1875rem; line-height: 1;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.label .s {
  font-family: var(--mono); font-size: 0.6875rem; color: var(--dim);
  margin-top: 4px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.add {
  flex: none; min-height: 46px; padding: 0 16px; font-size: 0.875rem;
}
.add[disabled] { background: var(--ok); color: #fff; border-color: var(--ok); cursor: default; }

/* Remove-from-inventory button (worker inventory search) */
.rm {
  flex: none; min-height: 46px; padding: 0 16px;
  background: var(--card); color: var(--red); border-color: var(--red);
  font-size: 0.9375rem;
}
@media (hover: hover) { .rm:hover { background: var(--red); color: #fff; } }

/* A track that was just removed collapses into a muted row */
.label.is-removed { opacity: .6; }
.pill.removed { color: #fff; background: var(--red); }

/* status + tags */
.ex, .pill {
  font-family: var(--mono); font-size: 0.5625rem; font-weight: 700;
  letter-spacing: .08em; text-transform: uppercase;
  padding: 2px 5px; border-radius: 3px; vertical-align: 2px; margin-left: 6px;
}
.ex   { color: #fff; background: var(--red); }
/* A hand-set rating is outlined rather than recoloured: it is the same
   fact as .ex, with a different source, and a second red would read as a
   different severity. */
.ex.is-set { box-shadow: inset 0 0 0 1px #fff, 0 0 0 1px var(--red); }
.pill { flex: none; margin-left: 0; }
.pill.dl     { color: var(--on-yellow); background: var(--yellow); }
.pill.ready  { color: #fff; background: var(--ok); }
.pill.failed { color: #fff; background: var(--red); }
/* Chained: not drawn on its own, so it reads as a property of the track
   rather than a status. Outlined, not filled, to sit below the status pill. */
.pill.chained { color: var(--ink); background: transparent; box-shadow: inset 0 0 0 1px var(--ink); }

/* An initial is not a marking. Six queue rows put six yellow chips on screen —
   more accent than the hazard stripe carries. */
.by {
  flex: none; font-family: var(--mono); font-size: 0.625rem; font-weight: 700;
  color: var(--dim); background: var(--paper-2);
  box-shadow: inset 0 0 0 1px var(--line);
  padding: 4px 6px; border-radius: 2px; letter-spacing: .04em;
}

/* Floor list */
.floorlist { margin-top: 30px; }
.floorlist h2 {
  display: flex; align-items: center; gap: 9px;
  font-family: var(--display); font-weight: 800; text-transform: uppercase;
  letter-spacing: .05em; font-size: 1.0625rem; margin: 0 0 12px;
}
.floor-rows { display: flex; flex-direction: column; }
.frow {
  display: flex; align-items: center; gap: 12px;
  padding: 12px 2px; border-bottom: 1px solid var(--line);
}
.frow .m { flex: 1; min-width: 0; }
.frow .t { font-weight: 700; font-size: 0.9375rem; }
.frow .s { font-family: var(--mono); font-size: 0.6875rem; color: var(--dim); margin-top: 2px; }

.empty {
  font-family: var(--mono); font-size: 0.8125rem; color: var(--dim);
  padding: 14px 0;
}

/* ============================================================
   Admin
   ============================================================ */

.admin-tag {
  font-family: var(--mono); font-size: 0.625rem; font-weight: 700; letter-spacing: .1em;
  text-transform: uppercase; background: var(--invert-bg); color: var(--invert-fg);
  padding: 3px 6px; border-radius: 3px; vertical-align: 3px; margin-left: 4px;
}
.admin-panel { margin-top: 30px; }
/* A legend screen-printed above the panel, not a title inside it. Archivo
   rather than the display face: a condensed face at this size with small-caps
   tracking reads cramped and airy at once. */
.admin-panel > h2 {
  font-family: var(--label-face); font-weight: 600; text-transform: uppercase;
  letter-spacing: .15em; font-size: 0.8125rem; margin: 0 0 12px; color: #DCE0E6;
  padding-bottom: 10px; border-bottom: 1px solid var(--line);
}
.panel-hint { color: var(--dim); font-size: 0.8125rem; margin: 0 0 12px; }

/* Playback controls */
.admin-controls { display: flex; flex-direction: column; gap: 14px; }
.ctl-row {
  display: flex; align-items: center; gap: 18px; flex-wrap: wrap;
  padding: 4px 2px;
}
.btn-ctl { min-height: 46px; padding: 0 20px; font-size: 0.8125rem; }

/* Shareable listen links (admin overview). The URL box has to be readable and
   tappable on a phone — that is where these get copied from — so it stays on
   its own line under the label rather than fighting the button for width. */
.linkrow { display: flex; flex-direction: column; gap: 4px; padding: 8px 0; }
.linkrow + .linkrow { border-top: 1px solid var(--line, rgba(0,0,0,.12)); }
.linkrow-head { display: flex; align-items: baseline; gap: 10px; flex-wrap: wrap; }
.linkrow-label {
  font-family: var(--label-face); font-weight: 600; text-transform: uppercase;
  letter-spacing: .06em; font-size: 0.8125rem;
}
.linkrow-hint { color: var(--dim); font-size: 0.75rem; }
.linkrow .ctl-row { gap: 10px; flex-wrap: nowrap; }
.linkbox {
  flex: 1; min-width: 0; min-height: 46px; padding: 0 12px;
  border: var(--edge) solid var(--line); border-radius: var(--radius);
  background: var(--paper, #fff); color: var(--ink);
  font-size: 0.8125rem; text-overflow: ellipsis;
}
.linkrow .btn-ctl { padding: 0 16px; flex: none; }

/* The Tidal device code. Read off a screen and typed into a phone, so it is
   large, spaced, and selectable as one unit. */
.devicecode {
  font-family: var(--mono);
  font-size: clamp(1.75rem, 8vw, 2.75rem);
  font-weight: 800;
  letter-spacing: .22em;
  text-align: center;
  margin: 14px 0;
  padding: 14px 10px;
  border: var(--edge) solid var(--line);
  border-radius: var(--radius);
  background: var(--paper, #fff);
  color: var(--ink);
  user-select: all;
  overflow-wrap: anywhere;
}
.vol { display: flex; flex-direction: column; gap: 6px; flex: 1; min-width: 180px; }
.vol > span {
  font-family: var(--mono); font-size: 0.75rem; color: var(--dim);
  text-transform: uppercase; letter-spacing: .06em;
}
.vol output { color: var(--ink); font-weight: 700; }
.vol input[type=range] { width: 100%; accent-color: var(--ink); height: 28px; }

/* Data tables. min-width makes narrow screens actually scroll the wrapper
   instead of crushing every cell into three wrapped lines. */
.table-wrap { overflow-x: auto; overscroll-behavior-x: contain; }
.dtable { width: 100%; min-width: 32rem; border-collapse: collapse; font-size: 0.875rem; }
.dtable th {
  text-align: left; font-family: var(--mono); font-size: 0.625rem; letter-spacing: .1em;
  text-transform: uppercase; color: var(--dim); font-weight: 700;
  padding: 6px 10px 6px 0; border-bottom: 1px solid var(--line);
}
.dtable td { padding: 10px 10px 10px 0; border-bottom: 1px solid var(--line); vertical-align: middle; }
.dtable tr.is-banned { opacity: .6; }
.cell-title { font-family: var(--label-face); font-weight: 600; text-transform: uppercase; font-size: 1rem; line-height: 1; }
.cell-sub { font-family: var(--mono); font-size: 0.6875rem; color: var(--dim); margin-top: 3px; }
.cell-by { font-size: 0.8125rem; color: var(--dim); }
.num { text-align: right; }
.cell-actions { text-align: right; white-space: nowrap; }
.tbtn {
  min-height: var(--tap-min); padding: 0 12px; margin-left: 6px;
  background: var(--card); color: var(--ink);
  border: 1px solid var(--line); border-radius: 3px;
  font-family: var(--body); font-weight: 600; font-size: 0.75rem; cursor: pointer;
}
.tbtn.danger { border-color: var(--red); color: var(--red-text); }
.tbtn.ok { border-color: var(--ok); color: var(--ok); }
@media (hover: hover) {
  .tbtn:hover { background: var(--paper-2); }
  .tbtn.danger:hover { background: var(--red); color: #fff; }
  .tbtn.ok:hover { background: var(--ok); color: #fff; }
}

/* Import + settings */
.import-box {
  width: 100%; min-height: 96px; resize: vertical;
  background: var(--card); border: var(--edge) solid var(--line); border-radius: var(--radius);
  padding: 12px 14px; font-family: var(--mono); font-size: 0.8125rem; color: var(--ink);
  margin-bottom: 12px;
}
.import-result { margin-top: 12px; display: flex; gap: 8px; flex-wrap: wrap; }
.check { display: flex; align-items: center; gap: 10px; font-size: 0.9375rem; margin-bottom: 16px; cursor: pointer; }
.check input { width: 22px; height: 22px; min-height: 0; accent-color: var(--ink); flex: none; }
.field-group { border: 1px solid var(--line); border-radius: var(--radius); padding: 14px; margin-bottom: 16px; }
.group-l {
  font-family: var(--label-face); font-weight: 600; text-transform: uppercase;
  letter-spacing: .04em; font-size: 0.875rem; margin-bottom: 12px;
}
.saved { font-family: var(--mono); font-size: 0.8125rem; color: var(--ok); margin: 0 0 14px; }
.check-note { font-family: var(--mono); font-size: 0.6875rem; color: var(--dim); margin: -8px 0 16px 32px; line-height: 1.5; }
.env-note { font-family: var(--mono); font-size: 0.75rem; color: var(--dim); margin: 0 0 12px; line-height: 1.55; }
.env-note code { background: var(--paper-2); border: 1px solid var(--line); padding: 0 4px; border-radius: 3px; color: var(--ink); }
.env-note b { color: var(--ink); }
.env-row {
  display: flex; justify-content: space-between; align-items: baseline; gap: var(--s3);
  font-family: var(--mono); font-size: 0.8125rem; color: var(--dim);
  padding: 8px 0; border-top: 1px solid var(--line);
}
.env-row b { color: var(--ink); font-weight: 700; word-break: break-all; text-align: right; }
#settings-form .btn-go { width: auto; padding: 0 22px; }

/* Schedule fieldsets */
.sched .check { margin-bottom: 8px; }
.sched .check-note { margin: 0 0 14px 32px; }
.day-row { display: flex; flex-wrap: wrap; gap: 8px; margin-bottom: 14px; }
.day {
  display: inline-flex; align-items: center; gap: 6px; cursor: pointer;
  border: 1px solid var(--line); border-radius: 999px; padding: 6px 14px;
  min-height: var(--tap-min);
  font-family: var(--mono); font-size: 0.75rem; user-select: none;
}
.day input { width: 16px; height: 16px; min-height: 0; accent-color: var(--ink); flex: none; }
.day:has(input:checked) { border-color: var(--ink); background: var(--paper-2); font-weight: 700; }
.time-row { display: flex; gap: 14px; flex-wrap: wrap; }
.field.time { flex: 1 1 8rem; margin-bottom: 0; }
.field.time input[type="time"] {
  font-family: var(--mono); font-variant-numeric: tabular-nums;
}

/* ============================================================
   Crate monogram + search states
   ============================================================ */

.crate {
  flex: none; width: 46px; height: 46px; position: relative;
  border: var(--edge) solid var(--line); border-radius: 3px;
  background: var(--paper-2); display: grid; place-items: center; overflow: hidden;
}
.crate::before {
  content: ""; position: absolute; top: 0; left: 0; right: 0; height: 5px;
  background: var(--yellow); border-bottom: 1px solid var(--on-yellow);
}
.crate .mg { font-family: var(--display); font-weight: 800; font-size: 1.05rem; color: var(--ink); line-height: 1; }
.crate .art { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; display: block; }

/* Awaiting-approval screen */
.pending-msg { margin: 0 0 22px; line-height: 1.55; color: var(--dim); }
.pending-h { font-family: var(--display); font-weight: 800; text-transform: uppercase; font-size: 1.25rem; margin: 0 0 8px; color: var(--ink); }
.pending-msg b { color: var(--ink); }

/* ============================================================
   Admin — library management
   ============================================================ */

.lib-controls {
  display: flex; flex-wrap: wrap; gap: var(--s2); align-items: center; margin-bottom: var(--s4);
}
.lib-search { flex: 1; min-width: 190px; min-height: var(--tap-min); font-size: 0.875rem; }
.seg {
  display: inline-flex; border: var(--edge) solid var(--line);
  border-radius: var(--radius); overflow: hidden;
}
.seg-opt input { position: absolute; opacity: 0; width: 0; pointer-events: none; }
.seg-opt span {
  display: flex; align-items: center; min-height: var(--tap-min);
  padding: 0 14px; font-family: var(--mono); font-size: 0.6875rem;
  text-transform: uppercase; letter-spacing: .06em; color: var(--dim); cursor: pointer;
  border-left: 1px solid var(--line); user-select: none; white-space: nowrap;
}
.seg-opt:first-child span { border-left: 0; }
.seg-opt input:checked + span { background: var(--invert-bg); color: var(--invert-fg); }
.seg-opt input:focus-visible + span { outline: 3px solid var(--focus); outline-offset: -3px; }
.lib-sort {
  min-height: var(--tap-min); border: var(--edge) solid var(--line); border-radius: var(--radius);
  background: var(--card); color: var(--ink); font-family: var(--body); font-size: 0.875rem; padding: 0 10px;
}

.lib-summary { display: flex; flex-wrap: wrap; gap: var(--s2); margin-bottom: var(--s4); }
.sumchip {
  font-family: var(--mono); font-size: 0.6875rem; text-transform: uppercase; letter-spacing: .05em;
  color: var(--dim); background: var(--paper-2); border: 1px solid var(--line);
  padding: 5px 9px; border-radius: 3px;
}
.sumchip b { color: var(--ink); font-size: 0.8125rem; font-variant-numeric: tabular-nums; }
.sumchip.ok b   { color: var(--ok-text); }
.sumchip.dl b   { color: var(--warn-text); }
.sumchip.fail b { color: var(--red-text); }
.sumchip.dur { border-color: var(--ink); }

.lib-list { display: flex; flex-direction: column; gap: var(--s2); }
.libcard {
  display: flex; align-items: center; gap: var(--s3); flex-wrap: wrap;
  background: var(--card); border: var(--edge) solid var(--line);
  border-radius: var(--radius); padding: 11px 12px;
}
.libcard.is-banned { opacity: .62; }
.libcard .m { flex: 1; min-width: 55%; }
.libcard .t {
  font-family: var(--label-face); font-weight: 600; text-transform: uppercase;
  font-size: 1.125rem; line-height: 1.05;
}
.libcard .s { font-family: var(--mono); font-size: 0.6875rem; color: var(--dim); margin-top: 3px; }
.libmeta {
  display: flex; align-items: center; gap: var(--s2); margin-top: 6px;
  font-size: 0.6875rem; color: var(--dim); flex-wrap: wrap;
}
.acts { display: flex; gap: 6px; margin-left: auto; }

/* --- Compact library rows -------------------------------------------------
   Replaces the old six-line card. Two lines per track, with the edit form
   behind a <details> disclosure, so a screen shows a library rather than a
   handful of tracks. */
.lib-rows { display: flex; flex-direction: column; }
.librow {
  display: grid;
  grid-template-columns: auto minmax(0, 1fr) auto auto;
  align-items: center; gap: var(--s2);
  padding: 7px 8px; border-bottom: 1px solid var(--line);
}
.librow:hover { background: var(--paper-2); }
.librow.is-banned { opacity: .55; }
.librow .pick { display: flex; align-items: center; padding: 4px; cursor: pointer; }
.librow .pick input { width: 18px; height: 18px; min-height: 0; accent-color: var(--ink); flex: none; margin: 0; }
.rmain { min-width: 0; }
.rtop { display: flex; align-items: baseline; gap: var(--s2); flex-wrap: wrap; min-width: 0; }
.rt {
  font-family: var(--label-face); font-weight: 600; text-transform: uppercase;
  font-size: 1rem; line-height: 1.1;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap; max-width: 100%;
}
.ra { font-size: 0.8125rem; color: var(--dim); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.rmeta { display: flex; align-items: center; gap: 5px; flex-wrap: wrap; margin-top: 3px; font-size: 0.625rem; }
.rdim { color: var(--dim); }
.librow .acts { display: flex; gap: 5px; margin-left: 0; }
/* The disclosure keeps the edit form off screen until it's wanted. */
.rowedit > summary {
  list-style: none; cursor: pointer; user-select: none;
  font-family: var(--mono); font-size: 0.625rem; text-transform: uppercase;
  letter-spacing: .06em; color: var(--dim);
  border: 1px solid var(--line); border-radius: 3px; padding: 0 8px;
  min-height: var(--tap-min); display: flex; align-items: center;
}
.rowedit > summary::-webkit-details-marker { display: none; }
.rowedit[open] > summary { background: var(--invert-bg); color: var(--invert-fg); border-color: var(--invert-bg); }
.rowedit > .classify { grid-column: 1 / -1; margin-top: 8px; }
/* The form is a grid child, so opening it has to span the row. */
.librow > .rowedit { display: contents; }
.librow > .rowedit > summary { grid-column: 4; }

/* --- Bulk action bar ------------------------------------------------------
   Hidden until something is ticked (the island adds .is-armed), so it costs
   nothing while you're just reading. */
.bulkbar {
  display: none; align-items: center; gap: var(--s2); flex-wrap: wrap;
  position: sticky; top: 0; z-index: 5;
  background: var(--ink); color: var(--paper);
  padding: 8px 10px; border-radius: var(--radius); margin-bottom: var(--s2);
}
.bulkbar.is-armed { display: flex; }
.bulkbar .pick input { width: 18px; height: 18px; min-height: 0; accent-color: var(--yellow); }
.bulkcount { font-size: 0.6875rem; color: var(--bar-accent); min-width: 8ch; }
.bulkacts { display: flex; align-items: center; gap: 5px; flex-wrap: wrap; }
.bulkbar .tbtn { background: var(--paper); border-color: var(--paper); }
.bulkbar .cl-in { min-width: 110px; max-width: 150px; }

/* Audio attribute chips (genre / BPM / key / tags) + inline classify form. */
.attrs { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 7px; }
.attr {
  font-family: var(--mono); font-size: 0.625rem; text-transform: uppercase; letter-spacing: .04em;
  color: var(--dim); background: var(--paper-2); border: 1px solid var(--line);
  padding: 2px 6px; border-radius: 3px;
}
.attr.genre { color: var(--ink); border-color: var(--ink); }
.attr.tag { color: var(--ink-2); }
.classify { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 8px; align-items: center; }
.classify .cl-in {
  font-family: var(--mono); font-size: 0.6875rem; color: var(--ink);
  background: var(--card); border: 1px solid var(--line); border-radius: 3px;
  padding: 4px 8px; min-height: 34px; min-width: 90px; flex: 1 1 90px; max-width: 190px;
}
.classify .cl-in:focus { outline: none; border-color: var(--ink); }
/* The select sizes to its longest option; it must not also stretch. */
.classify .cl-sel { flex: 0 0 auto; min-width: 0; max-width: none; }

/* Admin section nav (Overview / Library / People / Settings). */
.anav {
  display: flex; gap: 0; flex-wrap: wrap;
  border-bottom: var(--edge) solid var(--line); background: var(--paper-2);
  padding: 0 var(--s3);
}
.anav-item {
  font-family: var(--label-face); font-weight: 600; text-transform: uppercase;
  letter-spacing: .04em; font-size: 0.875rem;
  background: none; border: 0; border-bottom: 3px solid transparent;
  color: var(--dim); padding: 0 14px; min-height: var(--tap-min); cursor: pointer;
}
@media (hover: hover) {
  .anav-item:hover { color: var(--ink); }
}
.anav-item.is-active { color: var(--ink); border-bottom-color: var(--yellow); }

/* Library scan row + banger-mode scope readout. */
.lib-head-row { display: flex; align-items: center; gap: var(--s2); margin: 0 0 var(--s3); flex-wrap: wrap; }
.scope-readout { font-family: var(--mono); font-size: 0.6875rem; color: var(--dim); margin: 10px 0 0; }
.scope-readout b { color: var(--ink); }
.scope-readout.warn { color: var(--red); }
.scope-readout.warn b { color: var(--red); }

/* Running-version panel. Monospace so build stamps and short SHAs line up and
   a changed digit is easy to spot between two visits. */
.vtable { width: 100%; border-collapse: collapse; font-size: 0.8125rem; }
.vtable td { padding: 6px 0; border-bottom: 1px solid var(--line); vertical-align: top; }
.vtable tr:last-child td { border-bottom: 0; }
.vtable td:first-child { color: var(--dim); width: 12rem; padding-right: var(--s2); }
.vtable td:last-child { font-family: var(--mono); font-size: 0.75rem; }
.vtable b { color: var(--ink); }
.vtable code { font-family: var(--mono); color: var(--warn-text); }
.dimtext { color: var(--dim); }
.ok-text { color: var(--ok); }
.bad-text { color: var(--red); }

/* ============================================================
   Toast
   ============================================================ */

.toast {
  position: fixed; left: 50%; transform: translateX(-50%);
  bottom: calc(20px + env(safe-area-inset-bottom));   /* clear the home indicator */
  max-width: 92vw;
  background: var(--ink); color: var(--paper);
  border: var(--edge) solid var(--line); border-left: 6px solid var(--yellow);
  border-radius: var(--radius); padding: 13px 16px;
  font-family: var(--body); font-weight: 500; font-size: 0.875rem;
  box-shadow: 0 10px 30px -12px rgba(0,0,0,.5); z-index: 50;
  animation: toast-in .18s ease;
}
.toast.ok  { border-left-color: var(--ok); }
.toast.err { border-left-color: var(--red); }
@keyframes toast-in { from { opacity: 0; transform: translate(-50%, 8px); } }

/* ============================================================
   Listen page — a focused public player
   ============================================================ */

/* ═══════════════════════════════════════════════════════════════════
   THE LISTEN PAGE

   A screen somebody opens and leaves open, so it is built as a display
   rather than as a form: the cover carries it, and the whole deck fits
   one window on desktop.

   The old version reused the app's manifest card. That card belongs in a
   column beside a sidebar; as a whole page it was a 544px box adrift on a
   1440px screen with the cover at 104px and 45% of the window empty below.
   ═══════════════════════════════════════════════════════════════════ */

.lp-head {
  position: relative;
  display: flex; align-items: center; justify-content: space-between; gap: var(--s4);
  padding: 13px clamp(14px, 4vw, 32px);
  background-color: var(--ano-700);
  background-image: linear-gradient(to bottom, rgba(var(--hi),.028), transparent 18px);
  border-bottom: 1px solid rgba(var(--lo),.7);
}
.lp-head::before {
  content: ""; position: absolute; inset: 0; pointer-events: none;
  opacity: .12; mix-blend-mode: overlay;
  background: var(--tex-brush) 0 0 / 300px 200px;
}
.lp-head > * { position: relative; }

.lp {
  flex: 1 1 auto; width: 100%; max-width: 1120px; margin: 0 auto;
  padding: clamp(16px,3vw,30px) clamp(14px,4vw,32px) 0;
  display: grid; grid-template-columns: 1fr; align-content: start;
  gap: clamp(18px,3vw,26px);
}
/* So the fragment's two halves become grid items of .lp rather than being
   wrapped in a box that would break the layout on every swap. */
#np-slot { display: contents; }
#np-slot .boot { grid-column: 1 / -1; text-align: center; }

.lp-art {
  position: relative; width: 100%; aspect-ratio: 1; overflow: hidden;
  background: var(--label-bg); border-radius: 3px;
  box-shadow: 0 0 0 1px rgba(var(--lo),.6), 0 2px 6px rgba(var(--lo),.5),
              0 24px 60px -28px rgba(var(--lo),.9);
}
.lp-art img { display: block; width: 100%; height: 100%; object-fit: cover; image-rendering: pixelated; }
/* No cover: the label stays, empty, rather than the layout collapsing. */
.lp-art.is-blank { background: var(--ano-800); box-shadow: 0 0 0 1px var(--line); }
/* The state chip rides ON the art, so the deck reads as one object instead of
   a stack of boxes. */
.lp-chip {
  position: absolute; left: 0; top: 0; z-index: 1;
  display: flex; align-items: center; gap: 8px; padding: 7px 12px 6px;
  background: var(--ano-950); color: var(--ink); border-radius: 0 0 3px 0;
  font-family: var(--label-face); font-weight: 600; font-size: 0.625rem;
  letter-spacing: .16em; text-transform: uppercase;
}
.lp-art[data-state="idle"] .live-dot,
.lp-art[data-state="offair"] .live-dot { background: #3A3D44; box-shadow: 0 0 0 1px rgba(var(--lo),.5); }

.lp-meta { min-width: 0; display: flex; flex-direction: column; gap: 14px; }
.lp-title {
  font-family: var(--display); font-weight: 800; text-transform: uppercase; margin: 0;
  font-size: clamp(2.1rem, 7vw, 3.6rem); line-height: .9; letter-spacing: .004em;
  text-wrap: balance;
}
.lp-artist { margin: 0; color: var(--dim); font-weight: 600; font-size: clamp(0.9375rem, 2.4vw, 1.125rem); }
.lp-consign {
  display: flex; justify-content: space-between; gap: 12px;
  font-size: 0.75rem; color: var(--dim); padding-top: 9px; border-top: 1px solid var(--line);
}
.lp-consign b { color: var(--ink); font-weight: 600; }

.lp-transport { display: flex; align-items: center; gap: var(--s3); }
.lp-up h2 {
  display: flex; align-items: center; gap: 9px; margin: 0 0 4px; padding-bottom: 10px;
  font-family: var(--label-face); font-weight: 600; font-size: 0.6875rem;
  letter-spacing: .16em; text-transform: uppercase; color: var(--dim);
  border-bottom: 1px solid var(--line);
}

/* ── Wide: two columns, and the whole thing inside one window ──────────────
   The art holds the left column for every row, so the queue fills the space
   beside it rather than the right column stopping short and leaving a hole.

   IT FITS THE WINDOW, at any window. Two things make that true rather than
   true-at-the-size-I-tested:
     1. The cover is sized from the HEIGHT as well as the width. A cover picked
        from vw alone is what makes a short laptop window scroll.
     2. The queue shows as many rows as there is room for, decided by
        min-height. On a short window that is one row — still the row that
        matters, the one playing next. */
@media (min-width: 56.25em) {
  .lp {
    grid-template-columns: minmax(0, var(--art)) minmax(0, 1fr);
    grid-template-areas: "art meta" "art transport" "art up";
    grid-template-rows: auto auto auto;
    align-content: center;
    column-gap: clamp(24px, 3.5vw, 40px);
    padding-bottom: clamp(20px, 3vw, 34px);
    --art: min(clamp(260px, 34vw, 520px), calc(100dvh - 300px));
  }
  .lp-art { grid-area: art; align-self: start; }
  .lp-meta { grid-area: meta; padding-top: 2px; }
  .lp-transport { grid-area: transport; }
  .lp-up { grid-area: up; }
}
/* How many queue rows fit. Each is ~48px; these are rows plus the deck above
   them rather than round numbers. */
@media (min-width: 56.25em) and (max-height: 47.99em) { .lp-up .qrow:nth-child(n+2) { display: none; } }
@media (min-width: 56.25em) and (min-height: 48em) and (max-height: 53.99em) { .lp-up .qrow:nth-child(n+3) { display: none; } }
@media (min-width: 56.25em) and (min-height: 54em) and (max-height: 61.99em) { .lp-up .qrow:nth-child(n+5) { display: none; } }

/* ── Narrow: the transport docks to the thumb ───────────────────────────── */
@media (max-width: 56.24em) {
  .lp { padding-bottom: 0; }
  .lp-transport {
    position: sticky; bottom: 0; z-index: 20;
    margin: 0 calc(-1 * clamp(14px,4vw,32px));
    padding: 12px clamp(14px,4vw,32px) calc(12px + env(safe-area-inset-bottom));
    background: color-mix(in srgb, var(--paper) 92%, transparent);
    backdrop-filter: blur(10px);
    border-top: 1px solid var(--line);
  }
  .lp-transport .playbtn { flex: 1 1 auto; justify-content: center; min-height: 58px; }
  /* The label wrapped to two lines once the button went full width. */
  .lp-transport .pp-label { white-space: nowrap; }
  .lp-up { padding-bottom: clamp(20px, 6vw, 32px); }
}

.playbtn {
  display: inline-flex; align-items: center; gap: var(--s3);
  min-height: 66px; padding: 0 var(--s6);
  color: var(--ink);
  background-color: var(--ano-500);
  background-image: var(--tex-brush), linear-gradient(180deg, rgba(var(--hi),.10), rgba(var(--lo),.22));
  background-size: 300px 200px, auto;
  background-blend-mode: overlay, normal;
  border: 0; border-radius: 999px;
  font-family: var(--label-face); font-weight: 600; text-transform: uppercase;
  letter-spacing: .12em; font-size: 0.9375rem; cursor: pointer;
  text-shadow: 0 -1px 0 rgba(var(--lo),.6);
  box-shadow: inset 0 1px 0 rgba(var(--hi),.16), 0 1px 2px rgba(var(--lo),.5),
              0 8px 20px -10px rgba(var(--lo),.65);
  transition: transform 90ms var(--ease), box-shadow .18s var(--ease), background-color .12s linear;
}
@media (hover: hover) { .playbtn:hover { background-color: var(--ink-2); } }
.playbtn:active { transform: translateY(1px); }
/* Lit while it is playing — the same emitted-light rule the manifest head
   uses. The button reports the radio's state by glowing rather than by being
   yellow all the time, which told you nothing. */
.playbtn[data-playing="true"] {
  color: var(--on-yellow);
  background-color: var(--lamp);
  background-image: linear-gradient(180deg, rgba(255,255,255,.22), rgba(var(--lo),.14));
  text-shadow: none;
  box-shadow: inset 0 1px 0 rgba(255,255,255,.35), 0 1px 2px rgba(var(--lo),.5),
              0 0 30px -6px color-mix(in srgb, var(--lamp) 60%, transparent);
}

/* Play = ink triangle; toggles to two pause bars when playing. */
.pp-icon {
  width: 0; height: 0; margin-left: 2px;
  border-style: solid; border-width: 11px 0 11px 18px;
  border-color: transparent transparent transparent var(--ink);
}
.playbtn[data-playing="true"] .pp-icon {
  width: 16px; height: 20px; margin-left: 0;
  border: 0; border-left: 6px solid var(--on-yellow); border-right: 6px solid var(--on-yellow);
}

.playstate { margin: 0; font-size: 0.75rem; color: var(--dim); }
/* Empty while simply playing — the chip and the lit button already say that —
   so the element takes no gap rather than leaving a hole beside the control. */
.playstate:empty { display: none; }

/* --- Track chains (admin) --------------------------------------------------
   A chain ships as one unit, so it is drawn as a run rather than a table: a
   coupling spine down the left, a filled marker on the head and hollow ones on
   what it pulls along. Order carries real information here (the head is the
   only track rotation can draw), which is what earns the numbering.
   --------------------------------------------------------------------------- */

.chain-summary {
  margin: 0 0 var(--s4);
  font-family: var(--mono); font-size: 0.75rem; letter-spacing: .02em;
  color: var(--dim); font-variant-numeric: tabular-nums;
}

.chain-new { display: flex; gap: var(--s2); flex-wrap: wrap; margin: 0 0 var(--s4); }
.chain-new input { flex: 1 1 14rem; min-width: 0; height: var(--tap-min); }
/* .btn-go is the full-width login primary; here it sits beside a field. */
.chain-new .btn-go { width: auto; flex: 0 0 auto; padding: 0 var(--s5); font-size: 1rem; min-height: var(--tap-min); }

.chain {
  background: var(--card);
  border: var(--edge) solid var(--line);
  border-radius: var(--radius);
  margin: 0 0 var(--s4);
  overflow: hidden;
}

/* Header reads as the label stapled to the top of the consignment. */
.chain-hd {
  display: flex; align-items: center; gap: var(--s3); flex-wrap: wrap;
  padding: var(--s2) var(--s3);
  min-height: 56px;
  background: var(--paper-2);
  border-bottom: var(--edge) solid var(--line);
}
/* The name edits in place. It looks like a heading until you reach for it,
   which is why it carries a hover and focus border rather than a Rename button. */
.chain-nm {
  flex: 1 1 12rem; min-width: 0;
  min-height: var(--tap-min);
  padding: 4px 6px;
  font-family: var(--display); font-weight: 800; font-size: 1.0625rem;
  text-transform: uppercase; letter-spacing: .04em;
  color: var(--ink); background: transparent;
  border: 1px solid transparent; border-bottom-color: var(--line); border-radius: 3px;
}
@media (hover: hover) { .chain-nm:hover { border-color: var(--line); background: var(--card); } }
.chain-nm:focus { outline: none; border-color: var(--ink); background: var(--card); }
.chain-meta {
  font-family: var(--mono); font-size: 0.6875rem; color: var(--dim);
  white-space: nowrap; font-variant-numeric: tabular-nums;
}

.chain-run { list-style: none; margin: 0; padding: var(--s2) var(--s3) var(--s3); }

.run-item {
  position: relative;
  display: grid;
  grid-template-columns: 1.1rem minmax(0, 1fr) auto auto;
  grid-template-areas: "n txt flags acts";
  align-items: center;
  column-gap: var(--s3); row-gap: var(--s2);
  padding: var(--s2) 0 var(--s2) 34px;
  min-height: 52px;
  --mark-y: 50%;
}
/* The coupling, in two segments per row: row top to marker, marker to row
   bottom. Tiling it this way means it joins up exactly whatever height the rows
   end up, which a single full-height line stops doing once one of them wraps. */
.run-item::before,
.run-item::after {
  content: ''; position: absolute; left: 11px; width: 2px;
  background: var(--ink); transform: translateX(-50%);
}
.run-item::before { top: 0; height: var(--mark-y); }
.run-item::after { top: var(--mark-y); bottom: 0; }
.run-item:first-child::before { display: none; }
.run-item:last-child::after { display: none; }

.run-mark {
  position: absolute; left: 4px; top: var(--mark-y); transform: translateY(-50%);
  width: 14px; height: 14px; border-radius: 50%;
  background: var(--card); border: var(--edge) solid var(--line);
  z-index: 1;
}
/* Filled = the one rotation can draw. Everything below it is cargo. */
.is-head .run-mark { background: var(--yellow); }

.run-n {
  grid-area: n;
  font-family: var(--mono); font-size: 0.6875rem; color: var(--dim);
  font-variant-numeric: tabular-nums; min-width: 1.1rem;
}
.run-txt { grid-area: txt; min-width: 0; display: flex; flex-direction: column; gap: 2px; }
.run-title { font-size: 0.9375rem; font-weight: 600; line-height: 1.3; overflow-wrap: anywhere; }
.run-sub { font-family: var(--mono); font-size: 0.6875rem; color: var(--dim); }

.run-tag {
  padding: 3px 6px; white-space: nowrap;
  font-family: var(--label-face); font-weight: 600; font-size: 0.625rem;
  text-transform: uppercase; letter-spacing: .06em;
  background: var(--yellow); color: var(--on-yellow); border-radius: 2px;
}
.run-warn {
  font-family: var(--mono); font-size: 0.625rem; letter-spacing: .06em;
  text-transform: uppercase; color: var(--red); white-space: nowrap;
}
.run-item.is-broken .run-title { color: var(--dim); text-decoration: line-through; }

.run-flags { grid-area: flags; display: flex; align-items: center; gap: var(--s2); }
.run-acts { grid-area: acts; display: flex; gap: 4px; }
.run-btn {
  width: 36px; height: 36px; padding: 0;
  display: inline-flex; align-items: center; justify-content: center;
  background: var(--card); color: var(--ink);
  border: 1px solid var(--line); border-radius: 3px;
  font-size: 0.9375rem; line-height: 1; cursor: pointer;
}
.run-btn:disabled { opacity: .28; cursor: default; }
@media (hover: hover) {
  .run-btn:not(:disabled):hover { background: var(--paper-2); }
  .run-btn-rm:not(:disabled):hover { background: var(--red); border-color: var(--red); color: #fff; }
}
@media (pointer: coarse) { .run-btn { width: var(--tap-min); height: var(--tap-min); } }

.chain-empty {
  margin: 0; padding: var(--s4) var(--s3);
  color: var(--dim); font-size: 0.875rem; line-height: 1.5;
}

/* Add box. Dashed rule marks it as the working edge of the card rather than
   another row of the run. */
.chain-add { padding: var(--s3); border-top: 1px dashed var(--line); }
.chain-search { width: 100%; min-height: var(--tap-min); }

.pick-list {
  list-style: none; margin: var(--s2) 0 0; padding: 0;
  border: 1px solid var(--line); border-radius: 3px; overflow: hidden;
}
.pick-list li + li { border-top: 1px solid var(--line); }
.pick-row {
  width: 100%; min-height: var(--tap-min);
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto auto;
  grid-template-areas: "title sub add";
  align-items: center; column-gap: var(--s3);
  padding: var(--s2) var(--s3);
  background: var(--card); color: var(--ink);
  border: 0; cursor: pointer; text-align: left;
  font-family: var(--body);
}
.pick-title { grid-area: title; }
.pick-sub { grid-area: sub; }
.pick-add { grid-area: add; }
@media (hover: hover) { .pick-row:hover { background: var(--ano-600); } }
.pick-row:focus-visible { outline: 3px solid var(--focus); outline-offset: -3px; }
.pick-title { min-width: 0; font-size: 0.875rem; overflow-wrap: anywhere; }
.pick-sub {
  font-family: var(--mono); font-size: 0.6875rem; color: var(--dim);
  white-space: nowrap; font-variant-numeric: tabular-nums;
}
.pick-add {
  font-family: var(--label-face); font-weight: 600; font-size: 0.6875rem;
  text-transform: uppercase; letter-spacing: .06em;
}
.pick-note { margin: var(--s2) 0 0; font-family: var(--mono); font-size: 0.6875rem; color: var(--dim); }

.chain-none {
  padding: var(--s5) var(--s4); text-align: center;
  border: var(--edge) dashed var(--line); border-radius: var(--radius);
}
.chain-none-hd {
  margin: 0 0 4px;
  font-family: var(--display); font-weight: 800; font-size: 1rem;
  text-transform: uppercase; letter-spacing: .05em;
}
.chain-none-sub { margin: 0; color: var(--dim); font-size: 0.875rem; }

/* Narrow: the title takes its own line so it never gets squeezed between the
   number and the buttons, and the controls sit under it at full tap size. */
@media (max-width: 34em) {
  /* Title takes the full width; flags and controls drop to a second line so
     nothing is squeezed between the number and three buttons. */
  .run-item {
    --mark-y: 1.7rem;
    grid-template-columns: 1.1rem minmax(0, 1fr) auto;
    grid-template-areas:
      "n     txt   txt"
      "flags flags acts";
  }
  .pick-row {
    grid-template-columns: minmax(0, 1fr) auto;
    grid-template-areas:
      "title add"
      "sub   add";
  }
  .pick-sub { white-space: normal; }

  /* --- Admin library on a phone ------------------------------------------
     The four-column row (tick / track / actions / edit) assumes there is width
     to share. On a 360px screen the three action buttons and the edit
     disclosure take it all, leaving the title column about 40px — every track
     rendered as "MI...", the Edit button fell off the right edge, and the
     attribute chips stacked one-per-line into a ~200px-tall row. A library view
     that shows five tracks per screen is not a library view.

     So the actions drop to their own line and the track gets the full width. */
  .librow {
    grid-template-columns: auto minmax(0, 1fr) auto;
    grid-template-areas:
      "pick main main"
      "acts acts edit";
    align-items: start;
    row-gap: 7px;
    padding: 10px 8px;
  }
  .librow .pick { grid-area: pick; }
  .rmain { grid-area: main; }
  /* Wrapping matters: three buttons plus Edit is close to the full width at
     360px, and one more (Retry, on a failed track) must not overflow. */
  .librow .acts { grid-area: acts; flex-wrap: wrap; gap: 6px; }
  .librow > .rowedit > summary { grid-area: edit; justify-self: end; }
  .librow .tbtn { margin-left: 0; padding: 0 10px; }
  /* Now that the title has the row to itself, let it use it. */
  .rt { font-size: 1.0625rem; }
  .ra { white-space: normal; }

  /* The status filter is five nowrap options in an `overflow: hidden` pill —
     about 450px of content. On a 320px screen that does not merely look tight:
     "Removed" is clipped off the end and there is no way to scroll to it, so
     the filter is unreachable. Let it wrap instead of hiding options. */
  .seg { flex-wrap: wrap; overflow: visible; }
  .seg-opt span { padding: 0 11px; }

  /* Version panel: the 12rem label column is most of a phone screen, so the
     label sits above its value instead of beside it. */
  .vtable td { display: block; border-bottom: 0; padding: 2px 0; }
  .vtable td:first-child { width: auto; padding-right: 0; font-size: 0.6875rem; }
  .vtable tr { display: block; padding: 7px 0; border-bottom: 1px solid var(--line); }
  .vtable tr:last-child { border-bottom: 0; }
}

/* --- The now-playing cover, printed as a label ----------------------------- */

/* min-width:0 so a long title wraps instead of forcing the flex row wider than
   the card — the classic flex-overflow trap. */
.mmeta { flex: 1 1 auto; min-width: 0; }

/* SQUARE BY WIDTH, and it SETS the row height rather than following it.
   The obvious construction — align-self:stretch plus aspect-ratio, letting
   the ratio work backwards from the stretched height — does not resolve: a
   flex item's main size is computed before stretching gives it a cross size,
   so there is no height for the ratio to derive a width from. It rendered as
   a tall portrait crop of a square cover.
   Driving it from the width instead is unambiguous in every engine, and
   .mstack distributing over the resulting height is what keeps the two
   columns ending level. */
.mart { flex: none; width: clamp(104px, 24%, 190px); aspect-ratio: 1; }
.artlabel {
  display: block; width: 100%; height: 100%;
  /* The image IS 128px of 1-bit halftone; scaling it smoothly would blur the
     dither into grey mush and lose the whole point. Keep the dots hard. */
  image-rendering: pixelated;
  image-rendering: -webkit-optimize-contrast;   /* older WebKit */
  background: var(--label-bg);
  border-radius: 2px;
  box-shadow: 0 0 0 1px rgba(var(--lo),.6), 0 1px 3px rgba(var(--lo),.55);
  object-fit: cover;
}

/* --- Account: passkeys ----------------------------------------------------- */

/* The account page is one column of panels, not the app's two-up grid. */
.app-grid.one-col { grid-template-columns: 1fr; grid-template-areas: none; grid-template-rows: none; }   /* the desktop step's areas would auto-place the panels into side columns */

.keylist { list-style: none; margin: 0 0 12px; padding: 0; display: grid; gap: 8px; }
.keylist li {
  display: flex; align-items: center; gap: 10px; flex-wrap: wrap;
  padding: 10px 12px; border: 1px solid var(--line);
}
.keylist .keyname { font-weight: 600; }
/* The dates are the only way to tell two identical-looking phones apart, so
   they get the room rather than being squeezed against the name. */
.keylist .keymeta { flex: 1 1 auto; color: var(--dim); font-size: 0.75rem; }

/* Reserved height, so a status message appearing does not shove the button it
   sits under. */
.pk-status { min-height: 1.2em; margin: 6px 0 0; font-size: 0.8125rem; color: var(--dim); }
.pk-status.bad { color: var(--red-text); }

.pk-label { flex: 1 1 180px; min-width: 0; padding: 0 12px; height: 38px;
  border: 1px solid var(--line); background: var(--paper); color: var(--ink);
  font: inherit; font-size: 0.875rem; }

/* Inline within a sentence, rather than the block link-btn used in nav rows. */
.link-btn.inline { display: inline; padding: 0; font-size: inherit; line-height: inherit; }

/* app passwords (account page): the secret shown once */
.secret-once { margin: 0 0 12px; padding: 10px 12px; border: 1px solid var(--line, #444); border-radius: 8px; }
.secret-once .secret { display: block; margin: 6px 0; font-size: 1.1rem; letter-spacing: .04em; user-select: all; }

