:root {
  --bg: #f6f7f9;
  --panel: #ffffff;
  --ink: #1b1f24;
  --muted: #6b7480;
  --line: #dfe3e8;
  --accent: #1f6feb;
  --accent-ink: #ffffff;
  --ok: #1a7f45;
  --warn: #b25f00;
  --bad: #b3261e;
  --radius: 10px;
  /* Shared by the playlist header and its rows - two separate grids that must
     agree on where each column starts. */
  /* idx, item, seconds, full, sound, schedule, conditions, enabled, x.
     Every cell has to be accounted for here: a row that appends one
     more child than this lists wraps it onto a second line, which is
     how the remove button ended up under the row once before. */
  --row-cols: 22px 22px minmax(0, 1fr) 62px 34px 34px 110px 110px 52px 22px;
  /* Media rows and their headings, in order: checkbox, thumbnail, name,
     tenant, type, size, modified, action. EIGHT cells and eight tracks - a row
     that appends one more than this lists wraps it onto a line of its own at
     the left, which is how the design button ended up under the row.
     Shared for the same reason --row-cols is - two grids with an `auto` track
     size it from their own content, so the heading and the row under it
     disagree by however much their text differs.
     The first track must match .m-pick's size: a checkbox wider than its track
     overflows into the gap and sits flush against the thumbnail. */
  --media-cols: 16px 68px minmax(0, 1fr) 116px 158px 88px 140px 58px;
  /* Native widgets - the date/time picker icons, scrollbars, the controls we
     do not paint ourselves - follow this, not our variables. Without it the
     calendar glyph stays near-black on a dark panel. Both keywords, so it
     tracks the OS the same way the palette above does. */
  color-scheme: light dark;
}
@media (prefers-color-scheme: dark) {
  :root {
    --bg: #14171c; --panel: #1c2027; --ink: #e6e9ee; --muted: #9aa4b2;
    --line: #2b313a; --accent: #4b8dff; --ok: #4cc38a; --warn: #e0a458;
    --bad: #ef6a63;
  }
}
* { box-sizing: border-box; }
body {
  margin: 0; background: var(--bg); color: var(--ink);
  /* 16px, not 15. The lists read small at 15 and everyone was reaching for
     Chrome's 110% zoom to work in them, which is the user telling you the
     default is wrong. Raised at the root so every size derived from it moves
     together rather than a dozen rules drifting apart. */
  font: 16px/1.5 "Segoe UI", system-ui, sans-serif;
}
.hidden { display: none !important; }
.muted { color: var(--muted); }
button { font: inherit; cursor: pointer; }
button.link {
  background: none; border: 0; color: var(--accent); padding: 0;
  text-decoration: underline;
}
button.primary {
  background: var(--accent); color: var(--accent-ink); border: 0;
  padding: 8px 18px; border-radius: var(--radius); font-weight: 600;
}
button.primary:disabled { opacity: .45; cursor: default; }
button.ghost {
  background: none; border: 1px solid var(--line); color: var(--ink);
  padding: 7px 16px; border-radius: var(--radius); font-weight: 600;
}
button.ghost:disabled { opacity: .45; cursor: default; }
#publish-all { background: var(--warn); }

/* login */
.login { display: grid; place-items: center; min-height: 100vh; padding: 20px; }
.card {
  background: var(--panel); border: 1px solid var(--line);
  border-radius: var(--radius); padding: 28px; width: min(360px, 100%);
  display: grid; gap: 14px;
}
.card label { display: grid; gap: 6px; font-size: 13px; color: var(--muted); }
.card input {
  font: inherit; padding: 9px 11px; border-radius: 8px;
  border: 1px solid var(--line); background: var(--bg); color: var(--ink);
}
h1 { font-size: 20px; margin: 0; font-weight: 700; }
h1 span { color: var(--muted); font-weight: 400; }
.error { color: var(--bad); margin: 0; font-size: 13px; min-height: 1em; }

/* frame */
/* No sidebar: areas are chosen from a menu in the banner, and the full width
   goes to the tables, which are what this tool actually is. */
.app { display: grid; grid-template-rows: auto 1fr; min-height: 100vh; }
header {
  position: relative;   /* the area menu is anchored to it */
  grid-column: 1 / -1; display: flex; justify-content: space-between;
  align-items: center; gap: 16px; padding: 12px 20px; background: var(--panel);
  border-bottom: 1px solid var(--line);
}
header h1 { margin-right: auto; }

/* Area menu. The button names the area you are in, so the banner answers
   "where am I" as well as the sidebar used to. */
.menu-button {
  display: flex; align-items: center; gap: 10px;
  background: none; border: 1px solid var(--line); color: var(--ink);
  padding: 7px 14px 7px 11px; border-radius: var(--radius); font-weight: 600;
  font-size: 14px;
}
.menu-button:hover { border-color: var(--accent); }
.menu-button[aria-expanded="true"] { border-color: var(--accent); color: var(--accent); }
.bars {
  width: 15px; height: 11px; position: relative; display: inline-block;
  border-top: 2px solid currentColor; border-bottom: 2px solid currentColor;
}
.bars::after {
  content: ""; position: absolute; left: 0; top: 4px;
  width: 100%; border-top: 2px solid currentColor;
}
.nav.menu {
  position: absolute; top: calc(100% - 4px); left: 20px; z-index: 40;
  min-width: 190px; padding: 6px; background: var(--panel);
  border: 1px solid var(--line); border-radius: var(--radius);
  box-shadow: 0 10px 28px rgba(0, 0, 0, .28);
}
.who { display: flex; gap: 14px; align-items: center; font-size: 13px; }
.acting { display: flex; gap: 7px; align-items: center; color: var(--muted); }
.acting select {
  font: inherit; font-size: 13px; font-weight: 600; padding: 4px 8px;
  border-radius: 8px; border: 1px solid var(--line); background: var(--bg);
  color: var(--ink);
}
/* The root context can change things for everyone, so it should not look
   identical to working inside a tenant. */
body.root-context header { border-bottom: 2px solid var(--warn); }
body.root-context .acting select { border-color: var(--warn); color: var(--warn); }
main { padding: 20px; min-width: 0; }
.empty { color: var(--muted); padding: 40px 0; }

.list { list-style: none; margin: 0; padding: 0; display: grid; gap: 4px; }
.list li button {
  width: 100%; text-align: left; background: none; border: 1px solid transparent;
  color: var(--ink); padding: 8px 10px; border-radius: 8px;
}
.list li button:hover { border-color: var(--line); }
.list li button.active { background: var(--accent); color: var(--accent-ink); }
.list .sub { font-size: 12px; color: var(--muted); display: block; }
.list li button.active .sub { color: rgba(255,255,255,.8); }
.players li { padding: 8px 10px; border-radius: 8px; border: 1px solid var(--line); }
.dot { width: 8px; height: 8px; border-radius: 50%; display: inline-block;
       margin-right: 7px; background: var(--muted); }
.dot.online { background: var(--ok); }
.dot.offline { background: var(--bad); }

/* editor */
.editor-head { display: flex; justify-content: space-between; align-items: flex-start;
               gap: 20px; margin-bottom: 18px; }
.editor-head h2 { margin: 0 0 2px; font-size: 19px; }
/* Whose it is, beside the name: a quieter weight and size, so it reads as a
   qualifier rather than part of the title. */
.editor-head h2 .owner-tag {
  font-size: 13px; font-weight: 400; color: var(--muted); margin-left: 6px;
}
.editor-head p { margin: 0; font-size: 13px; }
.actions { display: flex; gap: 12px; align-items: center; font-size: 13px; }
/* Not 1fr 1fr: the left pane is a search-and-pick list that needs a fixed,
   modest width, while the right one is a six-column table whose item names
   were being squeezed to three characters and an ellipsis. Capped rather than
   proportional so the table takes all the width a wider window offers. */
.panes { display: grid; grid-template-columns: minmax(280px, 420px) minmax(0, 1fr);
         gap: 18px; align-items: start; }
/* The playlist editor's two panes share a height, so the item list is as tall
   as the picker beside it rather than stopping at its own content. That is
   not only tidiness: the empty space below the rows is where you drop things,
   and a five-item playlist used to offer a very small target.
   Not the channels editor - `frames-first` is two tables of different lengths
   that have no reason to match. */
.panes:not(.frames-first) { align-items: stretch; }
/* A read-only playlist has no picker, so the items pane is the only child -
   and a lone child lands in the *first* column, which is capped at 420px.
   Everything inside then squeezes: the row grid's 1fr name track collapses to
   nothing, so the item names disappear entirely and the headings overlap.
   Seen by a publisher opening a playlist shared from another workgroup. */
.panes > .pane:only-child { grid-column: 1 / -1; }
.panes:not(.frames-first) > .pane { display: flex; flex-direction: column; }
.panes:not(.frames-first) > .pane > .playlist { flex: 1 1 auto; }
/* Channels: Screens is a five-column table, Frames is a ten-column one, so
   the width goes to Frames. (The name is a leftover from when Frames came
   first; the ordering changed when Screens was added above it.) */
.panes.frames-first { grid-template-columns: minmax(240px, 0.75fr) minmax(0, 1.6fr); }
/* A screen's name is short - "Screen 1", "Port side" - so it does not need a
   third of the row, and the space is worth more to the frame table. */
.zones.areas input.aname { max-width: 150px; }
/* And a frame name has to show a name, not three characters of one. */
.zones input.zname { min-width: 110px; }
/* And a table can still outgrow whatever it is given - scroll it rather than
   hide a column. */
/* A screen behind its frames, so a superwide layout reads as screens with
   frames in them rather than a row of unrelated rectangles. */
.area-box {
  position: absolute; border: 2px dashed var(--muted); border-radius: 4px;
  opacity: .55;
}
.area-box span {
  position: absolute; top: 2px; left: 6px; font-size: 10px;
  text-transform: uppercase; letter-spacing: .05em; color: var(--muted);
}
.frame.main { outline: 2px solid var(--accent); outline-offset: -2px; }

/* The *wrapper* scrolls, not the table. Making the table display:block turns
   its thead and tbody into two independent tables, and they then size their
   columns separately - headings drift away from the values underneath. */
.table-scroll { overflow-x: auto; }
.table-scroll table.zones { min-width: 600px; }
/* Layout wants the full width, and three panes in a two-column grid would
   otherwise wrap it under the first one. */
.panes.frames-first > .layout-pane { grid-column: 1 / -1; }
@media (max-width: 900px) { .panes, .panes.frames-first {
                              grid-template-columns: 1fr; } }
.pane { background: var(--panel); border: 1px solid var(--line);
        border-radius: var(--radius); padding: 16px; }
.pane h3 { margin: 0 0 10px; font-size: 14px; }
.hint { font-size: 12px; margin: 0 0 10px; }

/* The whole media pane stays a drop target - .dropstrip is the visible one in
   the header. Nothing marks the pane until something is dragged over it, and
   then both light up together. */
#dropzone.over {
  outline: 2px dashed var(--accent); outline-offset: -6px;
  background: rgba(31,111,235,.07);
}
/* Adding things is a row of buttons, above the library rather than inside it:
   the pane holds the list and nothing else. Same shape as the buttons on the
   players page - both rows say "make a new thing", so they should not be a
   link in one place and a button in the other. */
/* The pager claims the empty half of the add row: that space was doing
   nothing, and this belongs beside the library rather than above it. The row
   is right-aligned, so `margin-right: auto` is what keeps the two Add buttons
   where they were. */
.pager {
  display: flex; align-items: center; gap: 8px; margin-right: auto;
  font-size: 13px; color: var(--muted);
}
.pager:empty { display: none; }
/* The armed selection sits left of the buttons it applies to. */
.editor-buttons #pick-chosen { margin-right: auto; font-size: 13px; }
/* Arrows, not words: they sit either side of the range and read as a pair. */
.pager button.ghost.small { padding: 3px 10px; font-size: 14px; line-height: 1; }
.pager .pg-at { font-variant-numeric: tabular-nums; }
.pager .pg-per { display: flex; align-items: center; gap: 6px; }
.pager select {
  font: inherit; font-size: 13px; padding: 4px 8px; border-radius: 6px;
  border: 1px solid var(--line); background: var(--bg); color: var(--ink);
}

.add-row { justify-content: flex-end; margin: -6px 0 10px; gap: 10px;
           /* .actions sets 13px and its buttons inherit it; without the same
              here these came out three pixels taller than the identically
              styled ones on the players page. */
           font-size: 13px; }
/* The drop target in the header: one line tall, taking the space between the
   title and the filters, with Upload right beside it - the two ways of doing
   the same thing, together. */
.media-head { align-items: center; }
/* The original drop area, kept as it was - it read well - but sized to sit in
   the header row rather than as a banner across the top of the library. It
   carries `browse` itself, so there is no separate upload button: one place
   to look, two ways to use it. */
.dropstrip {
  flex: 1 1 auto; min-width: 160px; max-width: 420px;
  border: 2px dashed var(--line); border-radius: var(--radius);
  padding: 8px 14px; text-align: center;
}
.dropstrip p { margin: 0; font-size: 13px; line-height: 1.35; }
.dropstrip.over { border-color: var(--accent);
                  background: rgba(31,111,235,.07); }
/* The filters live beside the title now. Left alone they wrapped onto a
   second line - the search grows to fill, and the count is pushed right by a
   margin - which made the header 94 px tall on its own. Beside a title they
   want to be one compact row instead. */
.editor-head .actions #media-controls,
.editor-head .actions #player-controls,
.editor-head .actions #playlist-controls { display: flex; }
.editor-head .list-controls { flex-wrap: nowrap; margin: 0; }
.editor-head .list-controls input[type="search"] { flex: 0 1 200px; }
/* The count sits at the end of the row rather than pushed to the far right of
   a page-wide container. */
.list-controls .lc-count { margin-left: 6px; }
.editor-head .list-controls .lc-count { margin-left: 0; white-space: nowrap; }
/* A search input and a select do not agree on their own height - different
   default padding and box model - so on one row they sat a couple of pixels
   apart. Pin both, and centre them against the taller things beside them. */
/* Everywhere, not only in a header: the same row appears above the players
   list and in the playlist picker, and the misalignment came from the input,
   not from where it was put. */
.list-controls input[type="search"],
.list-controls select {
  height: 32px; line-height: 1; align-self: center;
  /* input.filter carries a 10px bottom margin - it was written for a search
     box sitting alone above a list. On a row beside other controls that
     margin makes the line 42px tall and leaves the search flush at the top
     while everything else centres in it. */
  margin: 0;
}

.media { list-style: none; margin: 0; padding: 0; display: grid; gap: 6px;
         max-height: 46vh; overflow-y: auto; }
/* These two lists *are* the page, so they get the page's height rather than a
   box of their own inside it: the window scrolls, not a pane within a pane.
   The 46vh above still governs the picker in the playlist editor, which does
   sit beside something else.

   It is also what keeps the column headings lined up. Inside a scrolling box
   the rows lose ~15px to the scrollbar while the heading row above it does
   not, so every column sat a little left of its heading. */
#media-list, #playlist-cards { max-height: none; overflow: visible; }
/* The picker beside the item list: it *does* need bounding, because it sits
   next to something else and the page should not scroll to reach the bottom
   of a list you are dragging from. Measured from the viewport rather than a
   flat 46vh, so it uses whatever the header leaves - roughly the height of
   the items list next to it. */
#source-list { max-height: calc(100vh - 300px); min-height: 240px; }
/* Column headings, on the same grid as the rows so they line up with them. */
/* Playlists: one line each, in their own tracks.

   Its own variable rather than borrowing --media-cols: the two lists show
   different facts, and sharing the tracks would mean a column added to one
   silently misaligns the other. Five cells, five tracks - the heading row and
   the rows are separate grids, so both have to agree. Order: thumbnail, name,
   items, tenant, modified. */
:root { --playlist-cols: 68px minmax(0, 1fr) 116px 136px 116px; }

/* Channels: layout, name, frames, dimensions, owner.

   Five cells, five tracks - the heading row and the rows are separate grids
   and both have to agree. There is no "shared with" column because channels
   cannot be shared: no channel_shares table exists, only an owner. */
:root { --channel-cols: 160px minmax(0, 1fr) 90px 120px 130px; }

.channel-head-row {
  display: grid; grid-template-columns: var(--channel-cols);
  gap: 10px; align-items: end; padding: 0 10px 6px;
  font-size: 11px; text-transform: uppercase; letter-spacing: .06em;
  color: var(--muted); margin-bottom: 6px;
  border: 1px solid transparent;
  border-bottom-color: var(--line);
}
.channel-head-row span:nth-child(3),
.channel-head-row span:nth-child(4),
.channel-head-row span:nth-child(5) { justify-self: end; }
.channel-row { cursor: pointer; }
.media-row.channel-row .line { grid-template-columns: var(--channel-cols); }
.media-row.channel-row .line .meta { justify-self: end; white-space: nowrap; }
.media-row.channel-row .line .name .meta { justify-self: start; }

/* The layout diagram. A frame is a filled block on the reference ground, so
   the shape of the channel is the whole message - no labels would fit at this
   size and the row's hover carries them. */
/* A constant cell, so every row is the same height whatever shape the channel
   is. The diagram is centred in it and sized by channelDiagram(). */
.chan-cell {
  display: flex; align-items: center; justify-content: center;
  width: 160px; height: 48px; flex: 0 0 auto;
}
.chan-map {
  position: relative; display: block;
  background: var(--bg); border: 1px solid var(--line); border-radius: 3px;
  overflow: hidden;
}
.chan-map i {
  position: absolute; background: var(--accent); opacity: .72;
}
/* Frames are separated by shade, not by a line.

   A 1px inset outline was the obvious way to keep touching frames apart, and
   it is wrong at this size: a 65px banner on a 1080px channel is under 3px
   here, so an outline top and bottom consumed the block and left a dark seam
   where the layout has no gap at all. Shade costs no pixels. */
.chan-map i:nth-child(even) { opacity: .45; }
/* A text frame is usually a thin strip; a different fill keeps it from
   reading as a small playlist frame. */
.chan-map i.text { background: var(--warn); opacity: .8; }
.chan-map.empty { border-style: dashed; width: 60px; height: 34px; }
/* The design picker: the same rows as any other pick-list, with a diagram in
   front of the name instead of a thumbnail. */
#ch-designs li { gap: 10px; }
/* Sizing is inline, from channelDiagram(); the cell just holds the space. */
#ch-designs .chan-cell { flex: 0 0 auto; }
#ch-designs .label { flex: 1 1 auto; }
#ch-designs .remove { background: none; border: 0; color: var(--muted);
                      font-size: 18px; line-height: 1; padding: 2px 8px;
                      cursor: pointer; flex: 0 0 auto; }
#ch-designs .remove:hover { color: var(--bad); }
/* A narrow modal for a question, rather than the 560px a form needs. */
.modal.narrow { width: min(420px, 100%); }
/* The players filter panel: one row of controls.

   The size is set here, once, for the whole row. `button` inherits its font
   while the selects had declared their own, so the two buttons on the right
   came out a size larger than the four controls on the left - the same trap
   `.add-row` and `.bulk-bar` carry a note about. */
.filter-grid { display: flex; flex-wrap: wrap; align-items: center; gap: 14px;
               font-size: 13px; }
/* Metadata multi-select. The menu is absolute so opening it does not push the
   player list down the page, and it scrolls at ten-ish rows - the vocabulary
   grows with the fleet and this has to hold fifty without becoming a page of
   its own. */
/* A build that cannot update itself. Red on the version and the notes, not
   the whole row: the counts beside it are still ordinary information, and a
   row that is entirely red reads as an error rather than a caution. */
.inv tr.no-ota td strong, .inv tr.no-ota td:nth-child(4) { color: var(--bad); }
.no-ota-note { color: var(--bad); }

.multi { position: relative; }
.multi > button.on { border-color: var(--accent); color: var(--accent); }
.multi-menu {
  position: absolute; z-index: 20; top: calc(100% + 6px); left: 0;
  min-width: 260px; max-width: 340px; padding: 10px;
  background: var(--panel); border: 1px solid var(--line);
  border-radius: var(--radius); box-shadow: 0 10px 30px rgba(0,0,0,.35);
}
.multi-menu.hidden { display: none; }
.multi-menu .meta { margin: 0 0 8px; }
.multi-menu .filter { margin-bottom: 8px; }
.multi-menu ul {
  list-style: none; margin: 0; padding: 0; max-height: 260px; overflow-y: auto;
  display: grid; gap: 2px;
}
.multi-menu li.hidden { display: none; }
.multi-menu li label { display: flex; align-items: center; gap: 8px;
                       padding: 4px 6px; border-radius: 6px; font-size: 13px; }
.multi-menu li label:hover { background: var(--bg); }
/* label.inline carries its own 12px for use elsewhere; in this row everything
   is one size, so the label is brought up to it rather than the buttons down. */
.filter-grid label.inline { font-size: 13px; }
.filter-grid select { font: inherit; padding: 5px 8px;
                      border-radius: 6px; border: 1px solid var(--line);
                      background: var(--bg); color: var(--ink); }
button.primary.danger { background: var(--bad); }
#ch-designs input { font: inherit; font-size: 12px; padding: 3px 6px;
                    border-radius: 6px; border: 1px solid var(--line);
                    background: var(--bg); color: var(--ink); }

.playlist-head-row {
  display: grid; grid-template-columns: var(--playlist-cols);
  gap: 10px; align-items: end; padding: 0 10px 6px;
  font-size: 11px; text-transform: uppercase; letter-spacing: .06em;
  color: var(--muted); margin-bottom: 6px;
  /* Transparent sides, so this box measures the same as a row: the rows carry
     a 1px border and this does not, which put every column one pixel left of
     its heading. Invisible, and the columns line up. */
  border: 1px solid transparent;
  border-bottom-color: var(--line);
}
/* Items, tenant and modified are read as values, so they line up right with
   the numbers under them. */
.playlist-head-row span:nth-child(3),
.playlist-head-row span:nth-child(4),
.playlist-head-row span:nth-child(5) { justify-self: end; }
.playlist-row { cursor: pointer; }
/* `.media-row.playlist-row`, not `.playlist-row`: these rows reuse .media-row
   for its border, padding and hover, and `.media-row .line` sets the media
   library's eight tracks. Same specificity would leave it to source order -
   and this block sits above that one, so it would lose. Two classes wins
   wherever either rule ends up. */
.media-row.playlist-row .line {
  grid-template-columns: var(--playlist-cols);
}
.media-row.playlist-row .line .meta { justify-self: end; white-space: nowrap; }
/* Not the generic right-aligned meta: this one sits with the name and has to
   stay left, and be the first thing to give up room when the row narrows. */
.media-row.playlist-row .line .name .meta.used {
  justify-self: start; overflow: hidden; text-overflow: ellipsis;
}

.media-head-row {
  display: grid; grid-template-columns: var(--media-cols);
  gap: 10px; align-items: end; padding: 0 10px 6px;
  font-size: 11px; text-transform: uppercase; letter-spacing: .06em;
  color: var(--muted); margin-bottom: 6px;
  /* See .playlist-head-row: the same 1px the rows spend on their border. */
  border: 1px solid transparent;
  border-bottom-color: var(--line);
}
.media-row .line .when { justify-self: end; white-space: nowrap; }
/* The owner reads as a label, not a measurement, so it stays left like the
   name. The arrow marks one shared with you rather than yours - what the
   "from X" badge beside the name used to say. */
/* .meta.owner, not .owner: the generic `.media-row .line .meta` rule sits
   later in this file and right-aligns everything it matches, so an equally
   specific rule here loses on source order. */
.media-row .line .meta.owner { justify-self: start; text-align: left;
                               white-space: nowrap; overflow: hidden;
                               text-overflow: ellipsis; }
/* The kind and modified cells are right-aligned in the rows, so their
   headings are too - otherwise the words sit at the left of a column whose
   contents sit at the right, which looks like a misaligned table. */
.media-head-row span:nth-child(5),
.media-head-row span:nth-child(6),
.media-head-row span:nth-child(7) { justify-self: end; }
.media li {
  display: flex; justify-content: space-between; align-items: center; gap: 10px;
  border: 1px solid var(--line); border-radius: 8px; padding: 8px 10px;
  cursor: grab; background: var(--bg);
}
.media li:active { cursor: grabbing; }
/* The picker carries the same tile as the other two lists, so its name cell
   is a row - tile, then label - and the ellipsis moves onto the label. */
.media .name { font-size: 13px; display: flex; align-items: center; gap: 9px;
               min-width: 0; }
.media .name .label { overflow: hidden; text-overflow: ellipsis;
                      white-space: nowrap; }
.media .meta { font-size: 11px; color: var(--muted); white-space: nowrap; }

.playlist { list-style: none; margin: 0; padding: 0; display: grid; gap: 6px;
            min-height: 120px; }
.playlist.over { outline: 2px dashed var(--accent); outline-offset: 4px;
                 border-radius: 8px; }
/* Same track sizes as a row, so the labels sit over their own columns. The
   two chips are otherwise a pair of identical "always" with nothing saying
   which is which. */
/* Same bar as the media library, sitting between the hint line and the column
   headings. */
.bulk-bar.items { margin: 4px 0 8px; }
.playlist-head {
  display: grid; grid-template-columns: var(--row-cols);
  gap: 8px; padding: 0 10px 4px; font-size: 11px; color: var(--muted);
  text-transform: uppercase; letter-spacing: .04em;
}
.playlist-head span:nth-child(4) { text-align: center; line-height: 1.15; }
.sub-head { font-size: 9px; opacity: .75; }
/* Distinguishable from a duration at a glance: it is a count, not seconds. */
.playlist li input.per-loop { border-style: dashed; }
/* Full, Schedule, Conditions, Enabled: the controls under these are centred
   in their track, so a left-aligned label sits visibly off its own column. */
.playlist-head span:nth-child(5),
.playlist-head span:nth-child(6),
.playlist-head span:nth-child(7),
.playlist-head span:nth-child(8) { text-align: center; }

/* index, name, seconds, schedule chip, condition chip, remove. The header and
   the rows are separate grids, so every track except the name has to be a
   fixed width or the two will not agree on where a column is. Keep the fixed
   ones tight: this panel is ~650px, and generous chip columns starve the name
   until it is three characters and an ellipsis. minmax(0,1fr) rather than 1fr
   so the name can shrink to its ellipsis instead of forcing the row wider. */
.playlist li {
  display: grid; grid-template-columns: var(--row-cols); gap: 8px;
  align-items: center; border: 1px solid var(--line); border-radius: 8px;
  padding: 8px 10px; background: var(--bg); cursor: grab;
}
.playlist li.dragging { opacity: .4; }
.playlist li.drop-before { border-top: 2px solid var(--accent); }
.playlist li.drop-after { border-bottom: 2px solid var(--accent); }
.playlist .idx { color: var(--muted); font-variant-numeric: tabular-nums;
                 font-size: 12px; }
.playlist .name { font-size: 13px; overflow: hidden; text-overflow: ellipsis;
                  white-space: nowrap; }
.playlist input.dur {
  width: 100%; font: inherit; font-size: 12px; padding: 4px 6px;
  border-radius: 6px; border: 1px solid var(--line); background: var(--panel);
  color: var(--ink);
}
.playlist .remove { background: none; border: 0; color: var(--muted);
                    font-size: 16px; line-height: 1; padding: 2px 6px; }
.playlist .remove:hover { color: var(--bad); }

/* nav + views */
.nav { display: grid; gap: 4px; margin-bottom: 6px; }
.nav button {
  text-align: left; background: none; border: 1px solid transparent;
  color: var(--ink); padding: 8px 10px; border-radius: 8px; font-weight: 600;
}
.nav button:hover { border-color: var(--line); }
.nav button.active { background: var(--accent); color: var(--accent-ink); }
.view-title { font-size: 19px; margin: 0 0 14px; }
.cards { display: grid; gap: 8px; }
.card-row {
  display: grid; gap: 3px; text-align: left; background: var(--panel);
  border: 1px solid var(--line); border-radius: var(--radius);
  padding: 12px 14px; color: var(--ink);
}
.card-row:hover { border-color: var(--accent); }
/* A card with a picture: the tile takes a column of its own and spans the
   stack of lines, so the text keeps the same left edge whether the playlist
   has a thumbnail or a blank box. Scoped to `.has-thumb`, so channel and
   player cards are untouched. */
.card-row.has-thumb { grid-template-columns: auto 1fr; column-gap: 12px; }
.card-row.has-thumb > .row-thumb { grid-row: 1 / -1; align-self: center; }
.card-row.has-thumb > :not(.row-thumb) { grid-column: 2; min-width: 0; }
.card-row .muted { font-size: 12px; }
.editor-head input {
  font: inherit; padding: 7px 10px; border-radius: 8px;
  border: 1px solid var(--line); background: var(--panel); color: var(--ink);
}
.tabs { display: flex; gap: 6px; margin-bottom: 10px; }
.tabs button {
  background: none; border: 1px solid var(--line); color: var(--muted);
  padding: 5px 12px; border-radius: 999px; font-size: 13px;
}
.tabs button.active { background: var(--accent); color: var(--accent-ink);
                      border-color: var(--accent); }

table.zones { width: 100%; border-collapse: collapse; font-size: 13px; }
table.zones th { text-align: left; font-size: 11px; text-transform: uppercase;
                 letter-spacing: .06em; color: var(--muted); padding: 4px 6px; }
table.zones td { padding: 6px; border-top: 1px solid var(--line); }
table.zones select {
  font: inherit; font-size: 13px; padding: 4px 6px; border-radius: 6px;
  border: 1px solid var(--line); background: var(--bg); color: var(--ink);
  max-width: 190px;
}

.preview { position: relative; background: #000; border-radius: 6px;
           overflow: hidden; width: 100%; }
.frame { position: absolute; display: flex; align-items: center;
         justify-content: center; font-size: 11px; color: #fff;
         border: 1px solid rgba(255,255,255,.5); box-sizing: border-box; }
.frame.off { outline: 2px dashed var(--bad); outline-offset: -2px; }
table.zones input.num { width: 62px; text-align: right; }
table.zones input {
  font: inherit; font-size: 13px; padding: 4px 6px; border-radius: 6px;
  border: 1px solid var(--line); background: var(--bg); color: var(--ink);
  width: 100%; min-width: 0;
}
table.zones input:disabled, table.zones select:disabled { opacity: .55; }
.frame.f0 { background: rgba(120,140,255,.55); }
.frame.f1 { background: rgba(90,200,180,.55); }
.frame.f2 { background: rgba(230,170,90,.55); }
.frame.f3 { background: rgba(220,120,180,.55); }

.playlist li.nested { border-style: dashed; border-color: var(--accent); }
.playlist li.nested .name { font-weight: 600; }

.web-add { margin-bottom: 12px; }
.web-add input { flex: 1; min-width: 0; }

input.filter {
  width: 100%; font: inherit; font-size: 13px; padding: 7px 10px;
  border-radius: 8px; border: 1px solid var(--line); background: var(--bg);
  color: var(--ink); margin-bottom: 10px;
}
/* `.src-head` and `.is-playlist` lived here for the picker's two groups. The
   picker is media only now - playlists are nested through their own button -
   so nothing emits either class and both rules are gone rather than left to be
   puzzled over later. */

/* conditions */
/* A row is a chain now: conditions joined by the opposite operator to the one
   at the top, on one line, so what belongs together looks like it does. The
   word between them is the whole explanation, so it should read as a word and
   not as another control. */
.cond-join {
  font-size: 12px; text-transform: uppercase; letter-spacing: .06em;
  color: var(--muted); padding: 0 2px; align-self: center;
}
.cond-row .link.chain { margin-left: 4px; white-space: nowrap; }

.cond-chip {
  background: none; border: 1px dashed var(--line); color: var(--muted);
  border-radius: 999px; padding: 3px 10px; font-size: 11px; max-width: 220px;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.cond-chip.set { border-style: solid; border-color: var(--accent);
                 color: var(--accent); }
/* Chips are content-sized, their columns are not - centre them so each sits
   under its own heading instead of drifting to the left of it, and cap them
   at the column so a long schedule ellipsises rather than pushing the name
   out of the row. */
.playlist li .full-flag { justify-self: center; }
.playlist li .cond-chip, .playlist li input.dur,
.playlist li .chip-cell { justify-self: center; }
.playlist li .cond-chip { max-width: 100%; }
/* Chip with the full rule under it: the chip stays short so the row reads,
   the line below carries what it actually says. */
.chip-cell { display: grid; gap: 3px; justify-items: center; max-width: 100%; }
.chip-detail { font-size: 10px; line-height: 1.25; color: var(--muted);
               text-align: center; }
/* HH:MM, not a locale-formatted time picker - see normalizeTime(). */
.cond-row input.time { flex: 0 0 auto; width: 78px; text-align: center;
                       font-variant-numeric: tabular-nums; }
/* Selector includes .playlist li deliberately: this holder *is* one, and the
   row rule below is more specific, so a bare .cond-holder loses and the
   editor gets squeezed into the 22px index column. */
.playlist li.cond-holder,
.cond-holder { display: block; list-style: none; padding: 12px 14px;
               border: 1px solid var(--accent); border-radius: 8px;
               background: var(--panel); }
.cond-editor { display: grid; gap: 6px; }

/* The schedule panel has the full row width, so lay its three groups out
   side by side and let the prose span. auto-fit collapses them back to a
   column on a narrow window without a second breakpoint. */
/* Dates and times share the top row; days go underneath, where seven buttons
   fit without breaking mid-row. justify-content:start keeps the groups
   together on a wide panel instead of drifting apart. */
.sched-holder .cond-editor {
  grid-template-columns: max-content max-content;
  justify-content: start; gap: 12px 44px; align-items: center;
}
.sched-holder .day-row { grid-column: 1 / -1; }
.editor-buttons { grid-column: 1 / -1; display: flex; gap: 16px;
                  align-items: center; }
/* An item belonging to another tenant. Marked, not hidden: it is legal, it
   plays, and the owner needs to know why they cannot re-add it. */
.foreign {
  margin-left: 8px; padding: 1px 7px; border-radius: 999px;
  font-size: 10px; text-transform: uppercase; letter-spacing: .04em;
  border: 1px solid var(--warn); color: var(--warn); white-space: nowrap;
}
/* Kept content the owner has no visibility of - a prompt to look, not an
   error. Distinct from .foreign, which says only where something came from. */
.review-flag {
  margin-left: 8px; padding: 1px 7px; border-radius: 999px;
  font-size: 10px; text-transform: uppercase; letter-spacing: .04em;
  border: 1px dashed var(--bad); color: var(--bad); white-space: nowrap;
  cursor: help;
}
/* Players: a status list, with everything editable behind the row. */
.player-row {
  display: grid; align-items: center; gap: 12px; cursor: pointer;
  grid-template-columns: 14px minmax(130px, 1.3fr) .9fr 1.1fr .8fr 1.2fr .6fr .9fr;
}
/* The header shares the row's grid and its horizontal padding, plus a
   transparent border standing in for the card's, so the labels land over
   the columns rather than one pixel off them. */
.player-head {
  padding: 0 14px 6px; border: 1px solid transparent; border-bottom: 0;
  cursor: default; font-size: 11px; text-transform: uppercase;
  letter-spacing: .04em; color: var(--muted);
}
.player-head span[title] { cursor: help; }
/* A live session, next to the last sign-in. Deliberately a small mark rather
   than a badge reading "signed in" - it means a browser could still act, not
   that anyone is there. */
.live-dot { color: var(--accent); cursor: help; }
/* Destructive, and pushed to the right so it is never the button next to
   Save that you hit by accident. */
/* The text designer: several lines of fields plus a preview, so it needs both
   width and a scroll of its own on a short screen. */
.modal.wide, .modal.designer { max-width: 760px; }
.modal.designer { max-height: 88vh; overflow-y: auto; }
.modal.wide .cond-row, .modal.designer .cond-row { flex-wrap: wrap; gap: 10px; }
.modal.wide input.num, .modal.designer input.num { width: 68px; }
.text-line {
  border: 1px solid var(--line); border-radius: 8px;
  padding: 8px 10px; margin-bottom: 8px;
}
.text-line .tl-template { flex: 1 1 260px; }
.text-line .line-no { min-width: 14px; }
.text-line .line-detail { margin-top: 6px; }
/* Full width and 16:9-ish, so what you see is the proportion the boat gets. */
.text-preview {
  display: block; width: 100%; border: 1px solid var(--line);
  border-radius: 8px; background: var(--bg); margin-bottom: 6px;
}
button.block { display: block; width: 100%; margin-bottom: 10px; }

/* Triggers get the full width: eight columns, and the first three are the
   ones you actually read. */
.trigger-pane { margin-top: 18px; }
.trigger-pane table { width: 100%; }
.trigger-pane .t-name, .trigger-pane .t-watch { min-width: 130px; }
.trigger-pane select { min-width: 130px; }
.trigger-pane input.num { width: 64px; }

/* One pane, one tab at a time - see renderSettings. The grid that used to be
   here put unrelated panes of wildly different heights side by side, so a long
   one stretched the row and left the short one beside a lake of empty space.
   Tabs remove the question rather than answering it. */
.settings-pane { padding: 16px 18px 20px; }
.settings-pane .tabs { margin-bottom: 16px; flex-wrap: wrap; }
.settings-pane .tab-body > .share-label:first-child { margin-top: 0; }
/* A settings table has the pane to itself now, and at full width a two-column
   table strands its values half a screen from their labels. */
.settings-pane .inv { width: 100%; max-width: 900px; table-layout: auto; }

/* One setting on one line: label, the field taking the slack, then the button.
   The enrolment code is a single short value, and stacking it under a
   paragraph made a three-line panel that dictated the height of its row. */
.setting-row {
  display: flex; align-items: center; gap: 10px; flex-wrap: wrap;
}
.setting-row label { font-size: 12px; color: var(--muted); }
.setting-row input {
  flex: 1 1 200px; min-width: 0;
  font: inherit; font-size: 13px; padding: 7px 10px; border-radius: 8px;
  border: 1px solid var(--line); background: var(--bg); color: var(--ink);
}
.setting-row input:focus { outline: none; border-color: var(--accent); }
/* Row actions stay on one line - "Edit… Delete" wrapping mid-pair reads as two
   separate things. */
.inv .u-actions { white-space: nowrap; }
.inv th { font-weight: 600; white-space: nowrap; }

/* Media updates are queued behind this channel's unpublished changes - the
   publish button is the thing that releases them, so it says so. */
#publish-all.has-waiting { border-color: var(--warn); }
#publish-all.has-waiting::after { content: " ·"; color: var(--warn); }

/* Bulk selection in the media list. Hidden until there is something to
   select, so the common case of a short library stays uncluttered. */
.bulk-bar {
  display: flex; align-items: center; gap: 14px; padding: 8px 12px;
  margin-bottom: 8px; border: 1px solid var(--line); border-radius: 8px;
  background: var(--panel);
  /* Same 13px as .add-row and .actions, and for the same reason: button
     inherits its font, so a container that says nothing leaves these a size
     larger than the identically styled buttons in the header above. */
  font-size: 13px;
}
.bulk-bar.hidden { display: none; }
/* The actions sit directly beside the count, not pushed to the far right.
   Flung to the opposite edge they read as belonging to the table heading
   rather than to the selection that just appeared. */

/* The one-of-many picker: a searchable list inside the standard modal. Scrolls
   at eight-ish rows - tall enough to choose without hunting, short enough that
   the confirm button stays on screen. */
.pick-list {
  list-style: none; margin: 0; padding: 4px; max-height: 320px;
  overflow-y: auto; border: 1px solid var(--line); border-radius: 8px;
  background: var(--bg);
}
.pick-list li {
  display: flex; align-items: center; justify-content: space-between;
  gap: 10px; padding: 8px 10px; border-radius: 6px; font-size: 13px;
}
.pick-list li[data-value] { cursor: pointer; }
.pick-list li[data-value]:hover { background: var(--panel); }
/* The selected row has to survive a redraw and read as chosen at a glance:
   the border alone was invisible against the hover state. */
.pick-list li.picked {
  /* Derived from --accent so it follows the theme, with a flat fallback for
     anything without color-mix. --accent-soft does not exist as a token. */
  background: rgba(31, 111, 235, .16);
  background: color-mix(in srgb, var(--accent) 18%, transparent);
  outline: 1px solid var(--accent);
}
.pick-list .label { overflow: hidden; text-overflow: ellipsis;
                    white-space: nowrap; }
.modal .filter { width: 100%; font: inherit; font-size: 13px;
                 padding: 6px 9px; border-radius: 6px;
                 border: 1px solid var(--line); background: var(--bg);
                 color: var(--ink); }
/* Real columns rather than space-between. Spreading four items across the full
   width put every name in a different place, so the eye had nothing to run
   down - the point of a list is that the names line up. */
.media-row .line {
  display: grid; align-items: center; gap: 10px;
  /* The last track is a fixed width and always present, even when the row has
     no action. Each row is its own grid, so an `auto` track sizes to that
     row's content and the metas end at a different place on every line. */
  grid-template-columns: var(--media-cols);
}
/* A flex row so the "in use" badge can sit beside the name. Only `.label` is
   allowed to shrink - `min-width: 0` is what lets a grid/flex child ellipsise
   at all - so a long filename truncates and the badge stays whole. */
.media-row .line .name {
  justify-self: start; text-align: left; min-width: 0;
  display: flex; align-items: center; gap: 10px;
}
.media-row .line .name .label {
  min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
/* On the line now, so it carries no indent or top margin of its own. */
.media-row .line .name .uses { margin: 0; flex: 0 0 auto; }
/* Capped: a long web URL would otherwise widen this track and squeeze the
   names it is meant to sit beside. The full value is the row's tooltip. */
.media-row .line .meta {
  justify-self: end; text-align: right; max-width: 44ch;
  overflow: hidden; text-overflow: ellipsis;
}
.media-row .line .link { justify-self: end; }
/* The selection checkbox at native size was a ~13px target - fiddly with a
   mouse, hopeless with a touchscreen. 18px, and the accent instead of the
   OS default blue. The gap that stands in for it on rows you cannot select
   grows with it, so names stay in one column either way. */
.media-row .m-pick, #select-all {
  width: 16px; height: 16px; accent-color: var(--accent); cursor: pointer;
  /* Native checkboxes carry a default margin, which made the input overflow
     its 16px grid track and eat into the gap before the thumbnail. */
  margin: 0;
}
.media-row .pick-gap { width: 16px; }
/* Second line: what uses it, under the name rather than under the checkbox. */
.media-row .uses { display: flex; flex-wrap: wrap; align-items: center;
                   /* 26 = the 16px checkbox column + its 10px gap, so this
                      line starts under the thumbnail like the name does. */
                   gap: 8px; margin: 4px 0 0 26px; font-size: 12px; }
.upload-names { margin: 8px 0 14px; padding-left: 18px; font-size: 13px; }
/* A list that reports on a folder has no natural length: one drop is three
   files and the next is two hundred, and the pane cannot be as tall as the
   biggest of those. Scroll it instead - and because the panes sit in a grid,
   one enormous list also drove the height of the row it shares, which is what
   left a lake of empty space beside it. */
.scroll-list { max-height: 220px; overflow-y: auto; overscroll-behavior: contain;
               padding-right: 6px; }
button.link.danger { color: var(--bad); }
button.link.danger:disabled { color: var(--muted); cursor: not-allowed;
                              text-decoration: none; }
.linkish.danger { color: var(--bad); margin-left: auto; }
.linkish.danger:hover { color: var(--bad); text-decoration-thickness: 2px; }
.editor-buttons { display: flex; align-items: center; gap: 12px; }
.player-row:hover { border-color: var(--accent); }
.player-head:hover { border-color: transparent; }
.player-row .behind { color: var(--warn); }
.player-row .tenant-cell { font-size: 12px; }
.player-panel { margin-top: 14px; border-color: var(--accent); }
.panel-head { display: flex; align-items: baseline; gap: 18px;
              flex-wrap: wrap; margin-bottom: 12px; }
.panel-head h3 { margin: 0; }
/* The one destructive control on the panel, pushed away from the tabs so a
   miss on "Played" cannot land on it. */
.panel-head .push-right { margin-left: auto; }
/* Rename sits beside Delete, and without this they touch. */
.panel-head .push-right + button { margin-left: 14px; }
/* .tabs is already defined above, as a pill that fills with the accent and
   uses --accent-ink for its text. Do not re-declare it here: an earlier
   version of this block set only the text colour to the accent, leaving the
   accent background from the first rule, and the active tab became blue on
   blue. Same selector, two places, one of them silently half-applied. */
.tabs button:hover:not(.active) { border-color: var(--accent); }
.tabs button .muted { font-size: 11px; }
.tabs button.active .muted { color: var(--accent-ink); opacity: .8; }
.stat { display: flex; gap: 12px; padding: 5px 0;
        border-bottom: 1px solid var(--line); font-size: 13px; }
.stat-label { flex: 0 0 130px; color: var(--muted); }
.bad-text { color: var(--bad); }
.inv { width: 100%; border-collapse: collapse; font-size: 13px; }
.inv th { text-align: left; font-size: 11px; text-transform: uppercase;
          letter-spacing: .04em; color: var(--muted); font-weight: 600;
          padding: 4px 8px 6px 0; }
.inv td { padding: 5px 8px 5px 0; border-top: 1px solid var(--line); }
.inv tfoot td { color: var(--muted); font-size: 12px; }
/* Members of a "play n of these" group: all delivered, a slice plays each
   pass, so the row is listed but visibly conditional. */
.inv tr.grouped td:first-child { color: var(--muted); opacity: .5; }
.group-tag {
  margin-left: 8px; padding: 1px 7px; border-radius: 999px; font-size: 10px;
  border: 1px dashed var(--accent); color: var(--accent); white-space: nowrap;
  cursor: help;
}

/* Designer popup. Modal because it is a focused edit of one thing, and the
   channel table behind it has no room for eight more controls. */
.modal-back {
  position: fixed; inset: 0; z-index: 60; display: grid; place-items: center;
  background: rgba(0, 0, 0, .55); padding: 20px;
}
.modal {
  background: var(--panel); border: 1px solid var(--line);
  border-radius: var(--radius); padding: 20px; width: min(560px, 100%);
  max-height: 90vh; overflow-y: auto; display: grid; gap: 8px;
}
.modal h3 { margin: 0 0 4px; font-size: 15px; }
/* The modal is a grid with its own gap, so nothing inside it carries margins:
   a browser-default 1em on a <p> fights the gap and lands somewhere between
   the two. And an empty note - the share warning only some selections raise -
   must take no room at all, or it reserves 37px of nothing. */
.modal p { margin: 0; }
.modal p:empty { display: none; }
.modal .meta { font-size: 12px; }
.modal .share-label { margin: 0; }
.modal .share-flags { margin: 0; font-size: 13px; }
.modal .field { display: flex; align-items: center; gap: 8px;
                font-size: 13px; color: var(--muted); }
.modal .field input[type=text], .modal .field input:not([type]) { flex: 1; }
.modal .field input, .modal .field select {
  font: inherit; font-size: 13px; padding: 5px 8px; border-radius: 6px;
  border: 1px solid var(--line); background: var(--bg); color: var(--ink);
}
.modal .field input[type=color] { padding: 2px; width: 42px; }

.share-label { font-size: 12px; margin: 10px 0 4px; }
.share-flags { display: flex; flex-wrap: wrap; gap: 14px; margin-bottom: 12px; }
button.small { padding: 5px 16px; font-size: 13px; border-radius: 8px; }
.sched-holder .cond-editor > p,
.sched-holder .cond-editor > button.linkish { grid-column: 1 / -1; }
.sched-holder .cond-editor > p { max-width: 78ch; }
/* Wraps because a chain gets long: better a second line than a panel
   that grows sideways. */
.cond-row { display: flex; gap: 6px; align-items: center;
            flex-wrap: wrap; }
.cond-row.head { margin-bottom: 4px; }
.cond-row.head .mode { width: 84px; }
.cond-row select:first-child { min-width: 150px; }
.cond-row select, .cond-row input {
  font: inherit; font-size: 12px; padding: 4px 6px; border-radius: 6px;
  border: 1px solid var(--line); background: var(--bg); color: var(--ink);
}
.cond-row input { flex: 1; min-width: 0; }
.cond-row code { font-size: 11px; }
/* The freshness qualifier on a variable condition. Indented and quieter,
   because it modifies the row above rather than standing as its own test. */
.cond-row.sub { margin: -2px 0 6px 22px; font-size: 12px; }
.cond-row.sub select:first-child { min-width: 0; }
.cond-row.sub input[type="checkbox"] { flex: 0 0 auto; }
.cond-row.sub label { cursor: pointer; }
.cond-row input.narrow { flex: 0 0 auto; width: 62px; text-align: center; }
.cond-preview {
  margin: 8px 0 0; font-size: 12px; padding-top: 8px;
  border-top: 1px solid var(--line);
}
.cond-preview.none { color: var(--warn); }

/* Item scheduling. Reuses .cond-holder / .cond-editor / .cond-row so a
   schedule panel and a condition panel are visibly the same kind of thing. */
.sched-holder .field {
  display: flex; align-items: center; gap: 6px;
  font-size: 12px; color: var(--muted);
}
.sched-holder .field input { flex: 0 0 auto; }
.sched-holder .field-label { font-size: 12px; color: var(--muted); }
.day-row { flex-wrap: wrap; }
.day-btn {
  font: inherit; font-size: 12px; padding: 3px 8px; border-radius: 6px;
  border: 1px dashed var(--line); background: var(--bg); color: var(--muted);
  cursor: pointer;
}
.day-btn.on { border-style: solid; border-color: var(--accent);
              color: var(--accent); }
.warn-note { font-size: 12px; color: var(--warn); }
/* A guessed type the value could not actually support - a 0 that may be a
   timestamp. Marked rather than guessed at, so the choice is made once and
   deliberately instead of discovered when a trigger never fires. */
.warn-inline { font-size: 11px; color: var(--warn); }
select.needs-choice { border-color: var(--warn); }
.linkish {
  justify-self: start; font: inherit; font-size: 12px; padding: 0;
  border: 0; background: none; color: var(--muted);
  text-decoration: underline; cursor: pointer;
}
.linkish:hover { color: var(--ink); }

.card-row.static { cursor: default; }
.card-row.static:hover { border-color: var(--line); }
label.inline { font-size: 12px; color: var(--muted); display: flex; gap: 8px;
               align-items: center; }
label.inline input {
  font: inherit; font-size: 13px; padding: 4px 8px; border-radius: 6px;
  border: 1px solid var(--line); background: var(--bg); color: var(--ink);
}
/* The checkbox that holds a build against the ten-build ceiling. The generic
   `label.inline input` rule dresses text inputs - border, padding, radius - and
   a checkbox wants none of it. */
/* The source's path, edited where it is shown. Monospace because it is a
   Windows path and a stray space at the end is otherwise invisible. */
.src-path { font: inherit; font-family: ui-monospace, Consolas, monospace;
            font-size: 12px; width: min(340px, 100%); padding: 4px 8px;
            border-radius: 6px; border: 1px solid var(--line);
            background: var(--bg); color: var(--ink); }
.src-save { margin-left: 8px; }

/* The counterpart of .warn-note: something that is going to work. */
.ok-note { color: var(--ok); font-size: 12px; }
/* Select, Confirm and the state line on one row, so the button belongs to the
   dropdown beside it rather than floating under the column. */
.cond-cell { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }
.cond-cell select { font: inherit; font-size: 13px; padding: 4px 8px;
                    border-radius: 6px; border: 1px solid var(--line);
                    background: var(--bg); color: var(--ink);
                    min-width: 220px; }
.sc-state { font-size: 12px; }

.keep-cell { cursor: pointer; }        /* gap comes from label.inline */
.keep-cell input { padding: 0; border: 0; border-radius: 0; background: none;
                   cursor: pointer; }

.save-state { font-size: 12px; color: var(--muted); max-width: 380px;
              text-align: right; }
.save-state.dirty { color: var(--warn); }
.save-state.bad { color: var(--bad); }

/* The publish line lives under the buttons in the header now, so it is a line
   of text rather than a banner: no box, no background, right-aligned with the
   buttons above it. It used to be a full-width panel saying one short
   sentence. */
.head-right { display: flex; flex-direction: column; align-items: flex-end;
              gap: 8px; }
.publish-bar {
  display: flex; flex-wrap: wrap; gap: 10px; align-items: center;
  justify-content: flex-end; font-size: 13px; color: var(--muted);
}
/* Danger as a ghost button rather than a red link: same shape as the rest,
   still unmistakable. */
button.ghost.danger { color: var(--bad); border-color: var(--bad); }
button.ghost.danger:disabled { color: var(--muted); border-color: var(--line); }
.publish-bar.quiet { color: var(--muted); }
/* Without a box, the warning has to be carried by the words themselves. */
.publish-bar.stale, .publish-bar.stale strong { color: var(--warn); }
button.primary.small { padding: 5px 12px; font-size: 12px; }

.media li.media-row {
  display: block; cursor: default; background: var(--bg);
}
/* Column layout lives with the other .media-row rules above - one place, or
   the two disagree and the later one silently wins. */
.uses { display: flex; flex-wrap: wrap; gap: 8px; align-items: center;
        margin-top: 5px; font-size: 12px; }
.uses button.link { font-size: 12px; }
.badge.use { border-color: var(--accent); color: var(--accent); }

.badge {
  font-size: 10px; font-weight: 600; padding: 2px 8px; border-radius: 999px;
  border: 1px solid var(--line); color: var(--muted); vertical-align: middle;
}
.badge.share { border-color: var(--accent); color: var(--accent); }
.readonly { font-size: 12px; margin: 6px 0 0; color: var(--warn); }
.share-box {
  background: var(--panel); border: 1px solid var(--accent);
  border-radius: var(--radius); padding: 12px 14px; margin-bottom: 14px;
  display: flex; flex-wrap: wrap; gap: 14px; align-items: center;
}
.share-box p { margin: 0; font-size: 12px; flex-basis: 100%; }
/* The update and groups panels are *stacked* content, not a row of controls.
   `.share-box` is a flex row because the playlist permissions bar wants one,
   and only its paragraphs claimed a full line - so everything else flowed
   beside whatever happened to fit. "Build tokens" ended up level with the
   Choose players button, in the middle of the panel, once the arm picker got
   narrow enough to leave room. */
#update-box, #groups-box {
  flex-direction: column; align-items: stretch; gap: 10px;
}
/* One size for the row, as in .filter-grid and .add-row: `button` inherits its
   font, so without this the flags and the Save button came out at the body's
   15px beside 12px labels. */
.share-box { font-size: 13px; }
.share-box label.inline { font-size: 13px; }
/* `.share-label` and `.share-flags` carry margins for the places they are
   stacked in a column - the media dialog, the metadata panel. Here they are
   items in a centred flex row, where a top-heavy margin just pushes them off
   the line: 3px down and 6px up respectively, which is exactly enough to look
   wrong without looking like anything in particular. */
.share-box > .share-label, .share-box > .share-flags { margin: 0; }
/* "Shared with:" is a label like "Owned by" beside it, so it takes the row's
   size rather than the 12px it uses when heading a stacked block. */
.share-box > .share-label { font-size: 13px; }
.share-state { color: var(--ok); font-weight: 600; margin-left: 6px; }

.chips { display: flex; flex-wrap: wrap; gap: 6px; align-items: center;
         margin-top: 2px; }
.chip {
  font-size: 11px; padding: 2px 9px; border-radius: 999px;
  background: var(--accent); color: var(--accent-ink);
}
/* Clickable variable names in the slide designer. Buttons, so they are
   reachable by keyboard, but they must not look like actions that save
   anything - they only type for you. */
.var-chips { display: flex; flex-wrap: wrap; gap: 6px; margin: 2px 0 8px; }
.var-chips .chip {
  font: inherit; font-size: 11px; cursor: pointer;
  border: 1px solid var(--line); background: var(--bg); color: var(--ink);
}
.var-chips .chip:hover { background: var(--accent); color: var(--accent-ink);
                         border-color: var(--accent); }

/* The shared library header: search, filters, sort, and how many matched.
   One control for every list, so they cannot drift apart. */
.list-controls {
  display: flex; flex-wrap: wrap; gap: 8px; align-items: center;
  margin: 10px 0 6px;
}
.list-controls input[type="search"], .list-controls select {
  font: inherit; font-size: 12px; padding: 5px 8px; border-radius: 6px;
  border: 1px solid var(--line); background: var(--bg); color: var(--ink);
}
/* Wide enough to read a filename, not so wide it owns the row - it used to
   grow to whatever was left, which on the players page was most of the width
   for a field holding one word. */
.list-controls input[type="search"] { flex: 0 1 280px; min-width: 0; }
.list-controls .lc-count { font-size: 12px; margin-left: auto; }

.flags { display: flex; flex-wrap: wrap; gap: 12px; margin-top: 4px; }
.flag { font-size: 12px; display: flex; gap: 5px; align-items: center;
        color: var(--ink); }

.toast {
  position: fixed; bottom: 20px; left: 50%; transform: translateX(-50%);
  background: var(--ink); color: var(--bg); padding: 10px 18px;
  border-radius: 999px; font-size: 13px; box-shadow: 0 4px 14px rgba(0,0,0,.2);
}
.toast.bad { background: var(--bad); color: #fff; }

/* Live upload progress: bottom-right, out of the toast's lane, so the
   per-file result toasts and the running bar can be read at the same time. */
.upload-progress {
  position: fixed; bottom: 20px; right: 20px; width: 280px;
  background: var(--panel); border: 1px solid var(--line);
  border-radius: var(--radius); padding: 12px 14px;
  box-shadow: 0 4px 14px rgba(0,0,0,.2); z-index: 40;
}
.upload-progress .up-name { font-size: 13px; margin-bottom: 8px;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.upload-progress .up-track { height: 6px; border-radius: 999px;
  background: var(--line); overflow: hidden; }
.upload-progress .up-fill { height: 100%; width: 0;
  background: var(--accent); transition: width .2s; }
.upload-progress .up-detail { font-size: 12px; margin-top: 6px; }

/* Disabling an item rather than deleting it. The row stays exactly where it
   was - same position, duration, schedule and conditions - so it has to read
   as "present but off", not as missing. Dimmed, not hidden. */
.playlist li.item-off { opacity: .45; }
.playlist li.item-off .name { text-decoration: line-through; }
.row-toggle {
  font: inherit; font-size: 10px; text-transform: uppercase;
  letter-spacing: .06em; padding: 2px 8px; border-radius: 999px;
  border: 1px solid var(--accent); background: none; color: var(--accent);
  cursor: pointer;
}
/* Full opacity on the control itself, or the way back out of the state is the
   hardest thing on the row to see. */
.playlist li.item-off .row-toggle {
  opacity: 1; border-color: var(--muted); color: var(--muted);
}
.row-toggle:hover { border-color: var(--ink); color: var(--ink); }

/* Sortable column headings. The arrow is a pseudo-element fed from the
   heading's own dataset, so nothing in the markup shifts when it appears and
   the columns do not twitch as you click between them. */
/* Sortable headings, wherever a table has them. */
.player-head [data-sort], .media-head-row [data-sort] {
  cursor: pointer; user-select: none;
}
.player-head [data-sort]:hover, .media-head-row [data-sort]:hover { color: var(--ink); }
.player-head [data-sort]:focus-visible, .media-head-row [data-sort]:focus-visible { outline: 1px solid var(--accent);
  outline-offset: 2px; border-radius: 3px; }
.player-head [data-sort].sorted, .media-head-row [data-sort].sorted { color: var(--ink); }
.player-head [data-sort]::after, .media-head-row [data-sort]::after { content: attr(data-arrow); margin-left: 5px;
  font-size: 10px; }

/* Item thumbnails. Inside the name cell rather than a track of their own, so
   the header and the rows keep agreeing on their columns. Fixed box with the
   picture contained: the tiles are already uniform, and a fixed box means a
   row that has no thumbnail does not sit at a different height. */
.playlist li .name { display: flex; align-items: center; gap: 9px; min-width: 0; }
.row-thumb {
  /* 68x38, up from 56x32. Large enough to tell two weather slides apart, and
     still short enough that a row is one line of text tall. */
  flex: 0 0 auto; width: 68px; height: 38px; object-fit: contain;
  border-radius: 4px; border: 1px solid var(--line); background: #101318;
}
.playlist li.item-off .row-thumb { filter: grayscale(1); }
/* The same box when there is no picture, so every name starts in one place.
   A quiet glyph says which kind it is rather than leaving a hole. */
.row-thumb.blank {
  display: flex; align-items: center; justify-content: center;
  color: var(--muted); font-size: 13px;
}

/* Media rows: the name is a button now, so it must still look like a name. */
.media-row .name-open {
  font: inherit; font-size: 13px; text-decoration: none; text-align: left;
  color: var(--ink); max-width: 100%; overflow: hidden;
  text-overflow: ellipsis; white-space: nowrap;
}
.media-row .name-open:hover { color: var(--accent); text-decoration: underline; }
.row-thumb.clickable { cursor: zoom-in; }
.row-thumb.clickable:hover { border-color: var(--accent); }
/* The usage list is folded away; the badge is the control that reveals it. */
.badge.use.as-button { cursor: pointer; background: none; }
.badge.use.as-button.open { border-style: dashed; }
.use-list { display: inline-flex; flex-wrap: wrap; gap: 8px; align-items: center; }
.modal.preview { max-width: min(92vw, 1200px); }
.preview-media {
  display: block; max-width: 100%; max-height: 70vh; margin: 6px 0;
  border-radius: 8px; border: 1px solid var(--line); background: #101318;
}

/* A player can be perfectly online and still have a dark screen. Yellow says
   "running, but not showing everything it was asked to"; red is reserved for
   nothing playing at all. Neither is the same as offline. */
.dot.screens-some { background: var(--warn); }
.dot.screens-none { background: var(--bad); box-shadow: 0 0 0 2px rgba(0,0,0,.25); }

/* "Who plays this" - the answer Scala will not give you. */
.sc-who-out:empty { display: none; }
.sc-who-out { margin-top: 6px; }
.sc-who-funnel { margin: 4px 0; width: auto; }
.sc-who-funnel td { padding: 1px 10px 1px 0; font-size: 0.92em; }
.sc-who-funnel td + td { text-align: right; padding-right: 6px; }
.sc-who-list { font-size: 0.92em; line-height: 1.5; margin-top: 4px; }
.sc-who-routes, .sc-who-missed { font-size: 0.9em; margin-top: 4px; line-height: 1.5; }
.sc-who-missed span { border-bottom: 1px dotted currentColor; cursor: help; }

/* A player a compiled build cannot go to: shown, because "where is that boat"
   is worse than seeing it greyed, but not selectable. */
.flag.arm-blocked { opacity: 0.65; cursor: not-allowed; }
.flag.arm-blocked input { cursor: not-allowed; }
