/* Festival Shoot Planner — same visual language as the Shoot Timer / Runsheet. */

:root {
  --bg: #0f1115;
  --panel: #1a1d24;
  --panel-2: #14171d;
  --panel-3: #262b36;
  --panel-border: #2a2e38;
  --text: #eef0f3;
  --dim: #8a90a0;
  --accent: #4f8dfd;
  --good: #3ecf8e;
  --warn: #f5a623;
  --warn-strong: #ff7a1a;
  --bad: #ff5c5c;
  --purple: #7c5cff;
  --orange: #ff8f3d;
  --teal: #2dd4bf;
  --pink: #ff2d95;
  --yellow: #f0d31e;
}
* { box-sizing: border-box; }
html, body {
  margin: 0; padding: 0;
  background: var(--bg);
  color: var(--text);
  font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
  min-height: 100%;
  -webkit-text-size-adjust: 100%;
}
.app { max-width: 1100px; margin: 0 auto; padding: 16px 16px 90px; }

/* A grid can break out of the reading column and span the window, while the
   headers and menus around it stay where they are. */
.bleed {
  width: 100vw;
  margin-left: calc(50% - 50vw);
  margin-right: calc(50% - 50vw);
  border-radius: 0 !important;
  border-left: none !important;
  border-right: none !important;
}
.app.narrow { max-width: 760px; }
/* The planner, unlike the phone-first pages above, is worked from a laptop
   far more than a phone — 1100px was leaving real space unused on anything
   bigger than that. 1440px matches a 13" MacBook's own logical width, which
   is the actual machine this mostly gets used on; Setup's side nav (further
   down) is what finally made the extra room worth taking, since a nav
   column needs somewhere to sit beside its content rather than on top of
   it. */
.app.wide { max-width: 1440px; }

h1 { font-size: 20px; font-weight: 600; margin: 0 0 4px; }
h2 { font-size: 13px; text-transform: uppercase; letter-spacing: 0.04em; color: var(--dim); margin: 0 0 10px; }
.subtitle { color: var(--dim); font-size: 13px; margin-bottom: 16px; }
a { color: var(--accent); }

/* ---- Controls ---- */
input, select, textarea {
  background: var(--panel);
  border: 1px solid var(--panel-border);
  color: var(--text);
  border-radius: 8px;
  padding: 10px 12px;
  font-size: 15px;
  font-family: inherit;
}
/* A dropdown reads as a form field by default — flatten it toward the same
   weight as the ghost buttons that usually sit next to it, with its own
   chevron standing in for the native one appearance:none drops. */
select {
  padding: 9px 30px 9px 14px;
  font-size: 14px;
  font-weight: 600;
  cursor: pointer;
  appearance: none;
  -webkit-appearance: none;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='8' viewBox='0 0 12 8'%3E%3Cpath d='M1 1.5L6 6.5L11 1.5' stroke='%238a90a0' stroke-width='1.6' fill='none' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
  background-repeat: no-repeat;
  background-position: right 11px center;
}
select:hover { border-color: var(--accent); }
input[type="number"] { width: 76px; }
textarea { width: 100%; min-height: 120px; resize: vertical; font-size: 13px; line-height: 1.5; }
label.field { display: flex; flex-direction: column; gap: 4px; font-size: 12px; color: var(--dim); }
label.field > span { text-transform: uppercase; letter-spacing: 0.04em; }

/* `.upload-btn` is a <label> wrapping a hidden file input — the only way to
   style a file picker. It has to read as a button, so it takes the button
   rules too; without it the label renders as bare unpadded text. */
button, .upload-btn {
  background: var(--panel);
  border: 1px solid var(--panel-border);
  color: var(--text);
  border-radius: 8px;
  padding: 10px 14px;
  font-size: 14px;
  cursor: pointer;
  font-family: inherit;
}
button:hover, .upload-btn:hover { border-color: var(--accent); }
button.primary, .upload-btn.primary { background: var(--accent); border-color: var(--accent); color: #fff; font-weight: 600; }
button.good { background: var(--good); border-color: var(--good); color: #04241a; font-weight: 600; }
button.bad { background: transparent; border-color: var(--bad); color: var(--bad); }
button.ghost, .upload-btn.ghost { background: transparent; }
/* The touch-only "grab one thing directly" tool, sitting above the rest of
   the Assign/Crew sidebar. Armed reads as a clearly different state, not
   just a hover — it's actively waiting on your very next touch. */
.touch-drag-tool { width: 100%; margin-bottom: 12px; }
.touch-drag-tool.armed {
  background: rgba(79, 141, 253, 0.16); border-color: var(--accent); color: var(--accent); font-weight: 600;
}
button:disabled { opacity: 0.4; cursor: not-allowed; }
/* Glyph buttons (✎ ✕ ⌖): laid out as a box with the glyph centred in it,
   rather than as a line of text with padding around it — a glyph from a
   fallback font (⌖ is one) carries its own line-height, which used to push
   its button taller than the ✕ beside it and drop the glyph off centre. */
button.icon {
  display: inline-flex; align-items: center; justify-content: center;
  padding: 0; min-width: 30px; height: 30px; font-size: 13px; line-height: 1;
}
/* Matches the dropdown styling next to it — same padding/weight, so a
   button and a select sitting in the same row read as one family. */
button.small { padding: 9px 14px; font-size: 14px; font-weight: 600; }

.row { display: flex; gap: 8px; align-items: center; flex-wrap: wrap; }
.row.tight { gap: 6px; }
/* Notice/Runsheet/Display share the top row with the day nav, which only exists
   on some tabs. Without this they sit right on Setup and left everywhere else. */
.top-actions { display: flex; gap: 6px; align-items: center; flex-wrap: wrap; margin-left: auto; }
/* The same three buttons behind one ⋯ on a phone, where four controls plus a
   day picker plus the festival name do not fit on a line and were shoving the
   title around. Hidden on a wide screen, where there is room to just show
   them — so exactly one of .top-actions and this is ever on screen, and the
   markup for the buttons themselves is shared (topActionButtons in
   planner.html) rather than written twice. */
.top-actions-more { display: none; position: relative; }
.top-more-btn { min-height: 40px; letter-spacing: 0.1em; }
.top-more-menu {
  position: absolute; z-index: 60; top: calc(100% + 6px); right: 0;
  display: flex; flex-direction: column; gap: 6px; min-width: 176px; padding: 8px;
  background: var(--panel); border: 1px solid var(--panel-border); border-radius: 10px;
  box-shadow: 0 18px 40px rgba(0, 0, 0, 0.45);
}
/* Runsheet and Display are links wrapped round buttons, so both halves have to
   fill the row or the tap target stops at the text. */
.top-more-menu a { display: block; text-decoration: none; }
.top-more-menu button { width: 100%; text-align: left; min-height: 40px; }
/* The row has to claim the whole line, or on tabs where it wraps below the
   title it shrinks to its content and margin-left:auto has nothing to push
   against — which is how the buttons ended up left on Assign and Crew. */
.top-bar > .row.tight { flex: 1 1 auto; }
.spacer { flex: 1; }

/* ---- Top bar / nav ---- */
.top-bar {
  display: flex; justify-content: space-between; align-items: center;
  gap: 10px; flex-wrap: wrap; margin-bottom: 12px;
}
.planner-title { display: flex; align-items: center; gap: 10px; min-width: 0; }
.planner-back {
  display: inline-grid; place-items: center; flex: 0 0 32px; width: 32px; height: 32px;
  border: 1px solid var(--panel-border); border-radius: 9px; color: var(--dim);
  text-decoration: none; font-size: 17px;
}
.planner-back:hover { color: var(--text); border-color: var(--muted); }
.title-block h1 { margin-bottom: 2px; }
.title-block .subtitle { margin-bottom: 0; }
.title-block h1 .sync-pill { vertical-align: middle; margin-left: 8px; }

.sync-pill {
  font-size: 11px; font-weight: 700; letter-spacing: 0.04em; text-transform: uppercase;
  padding: 4px 9px; border-radius: 999px; border: 1px solid var(--panel-border); color: var(--dim);
  white-space: nowrap;
}
.sync-pill.saving { color: var(--warn); border-color: var(--warn); }
/* Three states, and they have to look as different as they read — see
   setSync() in app.js for why "Live" is not just a prettier "Up to date".
   Live is the green one with a lit dot; up-to-date keeps the same green text
   without it, so a glance separates "everyone can see this" from "it is
   saved, and nobody is watching". */
.sync-pill.saved { color: var(--good); border-color: #21503c; }
.sync-pill.live { color: var(--good); border-color: var(--good); }
.sync-pill.live::before {
  content: ""; display: inline-block; width: 6px; height: 6px; border-radius: 999px;
  background: var(--good); margin-right: 6px; vertical-align: 1px;
}
/* This device's own copy is newer than the server's — saved locally, not yet
   pushed. Amber like "saving" rather than green like "saved": it must not
   read as done, and not red like "offline" either, since nothing failed. */
.sync-pill.local-ahead { color: var(--warn); border-color: var(--warn); }
.sync-pill.offline { color: var(--bad); border-color: var(--bad); }
.sync-pill.locked { color: var(--bad); border-color: var(--bad); background: #241417; }
/* Not "offline": the server was reached and answered, it just had a newer plan
   than this device did. Amber rather than red because nothing is broken and
   nothing is lost — one change has to be made again. */
.sync-pill.conflict { color: var(--warn); border-color: var(--warn); background: #241f14; }
.sync-pill.demo { color: var(--accent); border-color: var(--accent); }

/* Who else is in this event's planner right now — see realtime.js's
   awareness wiring. Patched directly, not through render(), so it updates
   without disturbing whatever's actively being edited. */
.title-block h1 .presence-strip { vertical-align: middle; margin-left: 6px; display: inline-flex; gap: 4px; align-items: center; }
/* A dot until you hover it, then a pill with the whole name. The name is in
   the DOM the entire time and only ever clipped to zero width — expanding it
   in CSS rather than swapping the markup on hover means updatePresenceUI()
   rebuilding this strip mid-hover (which it does on every awareness change,
   including someone else's drag) can't interrupt the gesture. */
.presence-dot {
  display: inline-flex; align-items: center; justify-content: flex-start;
  height: 20px; min-width: 20px; border-radius: 999px;
  font-size: 10px; font-weight: 700; color: #0f1115;
  border: 1px solid rgba(0,0,0,0.25);
  overflow: hidden; white-space: nowrap;
  transition: box-shadow 0.12s ease;
}
.presence-dot .presence-initial { flex: 0 0 auto; width: 18px; text-align: center; }
.presence-dot .presence-name {
  max-width: 0; opacity: 0; overflow: hidden; padding-right: 0;
  font-weight: 600; letter-spacing: 0.01em;
  text-overflow: ellipsis; /* a name longer than the pill's cap trails off rather than cutting mid-letter */
  transition: max-width 0.18s ease, opacity 0.12s ease 0.03s, padding-right 0.18s ease;
}
.presence-dot:hover .presence-name,
.presence-dot:focus-visible .presence-name { max-width: 180px; opacity: 1; padding-right: 8px; }
/* Someone mid-drag: the suffix that used to live in the native title tooltip. */
.presence-dot .presence-doing { font-weight: 400; opacity: 0.75; }
.presence-dot.mine { box-shadow: 0 0 0 2px var(--panel-2), 0 0 0 3px var(--panel-border); }
.presence-dot.mine.editable { cursor: pointer; }
.presence-dot.mine.editable:hover { box-shadow: 0 0 0 2px var(--panel-2), 0 0 0 3px var(--dim); }
@media (prefers-reduced-motion: reduce) {
  .presence-dot, .presence-dot .presence-name { transition: none; }
}

/* ---- Activity log ----
   Who changed what, shared live. Anchored under the title so the panel hangs
   off the same corner the presence strip sits in, and patched directly rather
   than through render() for the same reason the strip is. */
/* Sits inline in the title next to the sync pill. The panel is anchored to
   .title-block rather than to this button, so it always drops from the title's
   left edge instead of from wherever the button happens to land — otherwise a
   long festival name would push a 420px panel off the right of a narrow screen. */
.title-block { position: relative; }
.title-block h1 .activity-mount { vertical-align: middle; margin-left: 6px; display: inline-flex; }
.activity-toggle {
  background: transparent; border: 1px solid var(--panel-border); color: var(--dim);
  border-radius: 999px; padding: 3px 11px; font-size: 12px; font-weight: 500; cursor: pointer;
  display: inline-flex; align-items: center; gap: 6px; line-height: 1.5;
}
.activity-toggle:hover { color: var(--text); border-color: var(--dim); }
.activity-toggle.open { color: var(--accent); border-color: var(--accent); background: rgba(79, 141, 253, 0.1); }
.activity-toggle:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.activity-count {
  background: var(--accent); color: #0f1115; font-weight: 700; font-size: 10px;
  border-radius: 999px; padding: 0 5px; min-width: 16px; text-align: center;
}
/* The transient notice: someone else changed something. Fixed to the bottom-left
   rather than hanging off the Activity button, for two reasons — it stops
   competing with the title row for space (anchored up there it had to stay
   short enough not to look wrong under a long festival name), and the bottom of
   the screen is where a notice can be ignored comfortably, which is the whole
   point of it. Left, not centre, so it never collides with the floating
   "+ Add set" button in the bottom-right.

   It lives inside .top-bar (which is .no-print), so it can't appear on a printed
   day sheet. Fixed positioning takes it out of flow, so the re-render that
   replaces its container doesn't move it. */
.activity-toast {
  position: fixed; z-index: 61; bottom: 20px; left: 20px;
  display: flex; align-items: baseline; gap: 7px;
  max-width: min(460px, calc(100vw - 40px));
  background: var(--panel); border: 1px solid var(--panel-border);
  border-radius: 999px; padding: 8px 16px 8px 13px;
  box-shadow: 0 12px 32px rgba(0,0,0,0.5);
  font-size: 12.5px; font-weight: 400; line-height: 1.4; color: var(--text);
  white-space: nowrap; overflow: hidden;
  animation: activity-toast-in 260ms cubic-bezier(0.2, 0.9, 0.3, 1.1);
}
.activity-toast .activity-toast-text { overflow: hidden; text-overflow: ellipsis; }
@keyframes activity-toast-in {
  from { opacity: 0; transform: translateY(14px); }
  to   { opacity: 1; transform: none; }
}
@media (prefers-reduced-motion: reduce) {
  .activity-toast { animation: none; }
}

.activity-panel {
  position: absolute; z-index: 60; top: calc(100% + 4px); left: 0;
  width: min(420px, calc(100vw - 32px));
  background: var(--panel); border: 1px solid var(--panel-border); border-radius: 10px;
  box-shadow: 0 18px 40px rgba(0,0,0,0.45);
  max-height: 60vh; overflow-y: auto;
}
.activity-list { list-style: none; margin: 0; padding: 6px; display: flex; flex-direction: column; gap: 1px; }
.activity-row {
  display: grid; grid-template-columns: 62px 1fr; gap: 10px; align-items: baseline;
  padding: 8px 10px; border-radius: 6px; font-size: 13px;
}
.activity-row:nth-child(odd) { background: rgba(255,255,255,0.02); }
.activity-when { color: var(--dim); font-size: 11px; font-variant-numeric: tabular-nums; }
/* Your own entries stay in the list — you need them to read the sequence —
   but sit back so other people's changes are what catches the eye. */
.activity-row.mine .activity-what { color: var(--dim); }
.activity-who { font-weight: 600; }
.activity-who::before {
  content: ""; display: inline-block; width: 7px; height: 7px; border-radius: 999px;
  margin-right: 6px; vertical-align: 1px; background: var(--who, var(--dim));
}
.activity-empty { padding: 16px 12px; color: var(--dim); font-size: 13px; text-align: center; }

/* ---- Lock screen ---- */
.lock-screen { max-width: 380px; margin: 12vh auto 0; text-align: center; }
.lock-screen .lock-icon { font-size: 34px; margin-bottom: 10px; }
.lock-screen h1 { font-size: 20px; margin-bottom: 6px; }
.lock-screen input { width: 100%; text-align: center; font-size: 20px; letter-spacing: 0.18em; padding: 14px; }
.lock-screen .actions { display: flex; gap: 8px; margin-top: 12px; }
.lock-screen .actions button { flex: 1; }
.lock-error { color: var(--bad); font-size: 13px; margin-top: 10px; min-height: 18px; }

.tabs { display: flex; gap: 8px; flex-wrap: wrap; margin-bottom: 14px; }
.tab {
  padding: 11px 20px; font-size: 15px; font-weight: 600; border-radius: 999px; background: transparent;
  border: 1px solid var(--panel-border); color: var(--dim); cursor: pointer;
}
.tab.active { border-color: var(--accent); color: var(--text); background: #16233a; }
/* Keyboard focus has to survive the phone rule below, where the strip becomes
   an overflow:auto box: a browser's default outline sits outside the button and
   would be clipped by it. Stated here, once, so it is the same ring at every
   width — the phone rule pays for it with padding rather than by hiding it. */
.tab:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }

@media (max-width: 600px) {
  /* One row that scrolls sideways, not two or three stacked. Six tabs at
     phone width wrapped to two rows and, with Delivery and Workload switched
     back on, three — 140px of a 812px-tall screen spent on navigation before
     the plan starts. Same technique .day-tabs and Setup's side nav already
     use at their own breakpoints.

     The scrollbar is hidden because it is a permanent 8px band under a row of
     pills that already look scrollable and, on the touch devices this rule is
     for, is not a thing anyone drags anyway. scrollActiveTabIntoView() in
     planner.html is what keeps the tab you are on in sight, since render()
     resets scrollLeft on every rebuild. */
  .tabs {
    flex-wrap: nowrap;
    overflow-x: auto;
    -webkit-overflow-scrolling: touch;
    scrollbar-width: none;
    scroll-snap-type: x proximity;
    /* Room for the focus ring above, which overflow:auto would otherwise cut
       off, and negative margins so the strip still lines up with the page. */
    padding: 3px; margin-left: -3px; margin-right: -3px;
  }
  .tabs::-webkit-scrollbar { display: none; }
  /* flex:none — a nowrap flex line would otherwise squeeze every tab until the
     labels ellipsed instead of letting the row overflow and scroll. */
  .tab { flex: 0 0 auto; scroll-snap-align: center; }

  /* The four top-right controls collapse into the ⋯ above. */
  .top-actions { display: none; }
  .top-actions-more { display: inline-flex; margin-left: auto; }
  /* And the day picker gives up whatever width the ⋯ needs, so the two share
     one line instead of the button wrapping to a row of its own. The select
     was sizing itself to its longest option (a whole day label plus its issue
     count) and there is not 341px plus a button in 375px. What the shorter box
     loses is the tail of that label — the count, which the coloured badge
     beside it is already saying, and which no longer has to be read out of an
     <option> at all. */
  /* nowrap on the row itself is the half that actually does it. A wrapping
     flex line breaks on the items' NATURAL widths, before any shrinking is
     considered — so a 341px picker and a 45px button in 343px went to two
     lines rather than one shared one, however shrinkable the select was made.
     Only the top bar's own row: .row.tight is a generic class used all over
     the app, and most of the places it appears want to wrap. */
  .top-bar > .row.tight { flex-wrap: nowrap; min-width: 0; }
  .day-picker { flex: 1 1 auto; min-width: 0; }
  /* basis 0, not auto. A <select>'s min-content width is its widest OPTION and
     stays that whatever min-width says, so with an auto basis the row's own
     automatic minimum was still the full 341px picker plus the button and the
     page scrolled sideways instead. Given a zero basis the box is simply
     whatever is left over after the arrows, the badge and the ⋯, with a floor
     so it can never collapse to a bare chevron. */
  .day-picker .day-select { flex: 1 1 0; min-width: 72px; }
}

.day-tabs { display: flex; gap: 6px; flex-wrap: wrap; margin-bottom: 14px; align-items: center; }
.day-tab {
  padding: 7px 12px; font-size: 13px; border-radius: 8px; background: transparent;
  border: 1px solid var(--panel-border); color: var(--dim); cursor: pointer; text-align: left;
}
.day-tab.active { border-color: var(--accent); background: #16233a; color: var(--text); }
.day-tab b { display: block; font-size: 13px; font-weight: 600; }
@media (max-width: 640px) {
  .day-tab { padding: 8px 14px; flex: 0 0 auto; }
  /* keep the schedule-mode buttons on one line on a phone */
  .day-tabs { flex-wrap: nowrap; overflow-x: auto; -webkit-overflow-scrolling: touch; }
  .day-tabs::-webkit-scrollbar { display: none; }
}
.broadcast-banner { align-items: center; }

/* ---- Stat strip ---- */
.stats { display: grid; grid-template-columns: repeat(auto-fit, minmax(120px, 1fr)); gap: 8px; margin-bottom: 16px; }
.stat {
  background: var(--panel); border: 1px solid var(--panel-border); border-radius: 10px; padding: 10px 12px;
}
.stat .k { font-size: 11px; text-transform: uppercase; letter-spacing: 0.04em; color: var(--dim); }
.stat .v { font-size: 20px; font-weight: 700; font-variant-numeric: tabular-nums; margin-top: 2px; }
.stat .v.good { color: var(--good); }
.stat .v.warn { color: var(--warn); }
.stat .v.bad { color: var(--bad); }

/* One quiet line of numbers instead of a grid of tiles. */
.summary {
  display: flex; flex-wrap: wrap; gap: 14px; align-items: baseline;
  margin-bottom: 14px; font-size: 13px; color: var(--dim);
}
.summary b { font-size: 16px; font-weight: 700; color: var(--text); margin-right: 3px; }
.summary .ok b { color: var(--good); }
.summary .warn b { color: var(--warn); }
.summary .bad b { color: var(--bad); }
.summary .dim b { color: var(--dim); }

.issue-bar { cursor: pointer; margin-bottom: 14px; }
.issue-bar:hover { border-color: var(--accent); }
.note-line { color: var(--dim); font-size: 12px; }

/* ---- Cards / list ---- */
.card {
  background: var(--panel); border: 1px solid var(--panel-border); border-radius: 10px;
  padding: 12px; margin-bottom: 8px;
}
.card.accent { border-color: var(--accent); background: #14202f; }
.panel {
  background: var(--panel-2); border: 1px solid var(--panel-border); border-radius: 12px;
  padding: 14px; margin-bottom: 14px;
}
/* Hazard stripes — the same "this isn't a normal control" language as a
   blockout block, just for a whole panel instead of one chip. */
.danger-zone {
  background-color: #0a0505;
  background-image: repeating-linear-gradient(135deg, rgba(255, 92, 92, 0.09) 0 10px, transparent 10px 20px);
  border: 1px solid var(--bad);
}
.danger-zone h2 { color: var(--bad); }
/* Admin settings, locked — same hazard-stripe language as the danger zone,
   tinted purple instead of red so it doesn't read as "something's wrong",
   just "something's gated". Once unlocked it switches to a plain purple
   panel instead — the nested .danger-zone block inside it (wipe/delete)
   keeps its own red styling regardless, from the rule above. */
.admin-panel-locked {
  background-color: #0d0a14;
  background-image: repeating-linear-gradient(135deg, rgba(124, 92, 255, 0.10) 0 10px, transparent 10px 20px);
  border: 1px solid var(--panel-border);
}
.admin-panel-unlocked { background: var(--panel-2); border: 1px solid var(--purple); }
.admin-panel-unlocked h2 { color: var(--purple); }
/* Sub-sections inside a gated panel like Admin settings — same idea as
   .panel's own h2, just nested one level, with a border to separate each
   block from the one above it instead of its own background/padding. */
.admin-settings-block { padding-top: 14px; margin-top: 14px; border-top: 1px solid var(--panel-border); }
.admin-settings-block:first-of-type { padding-top: 0; margin-top: 0; border-top: none; }
.admin-settings-block h3 { font-size: 13px; text-transform: uppercase; letter-spacing: 0.04em; color: var(--dim); margin: 0 0 10px; }
.admin-settings-block.danger-zone { padding: 14px; margin-top: 14px; border: 1px solid var(--bad); border-radius: 8px; }
.admin-settings-block.danger-zone h3 { color: var(--bad); }
/* Set apart the same way the danger zone is, but amber rather than red — this
   is reversible, it just isn't something to reach for on a live event. Dashed
   so it still reads as a different kind of block, not a second danger zone. */
.admin-settings-block.testing-block { padding: 14px; margin-top: 14px; border: 1px dashed var(--warn); border-radius: 8px; }
.admin-settings-block.testing-block h3 { color: var(--warn); }
.admin-settings-block.testing-block .field { flex-direction: row; align-items: center; gap: 8px; }
.admin-code-table { width: 100%; border-collapse: collapse; font-size: 13px; }
.admin-code-table th {
  text-align: left; font-size: 11px; text-transform: uppercase; letter-spacing: 0.04em;
  color: var(--dim); font-weight: 700; padding: 0 8px 6px; border-bottom: 1px solid var(--panel-border);
}
.admin-code-table th:first-child { width: 28px; }
.admin-code-table td { padding: 8px; border-bottom: 1px solid var(--panel-border); vertical-align: middle; }
.admin-code-table tr:last-child td { border-bottom: none; }
.admin-code-table td:first-child { color: var(--dim); }
.admin-code-actions { text-align: right; white-space: nowrap; }
.admin-code-hidden { color: var(--dim); font-size: 12px; letter-spacing: 0.04em; cursor: help; }

/* ---- Setup: side nav ----
 * The same dark, rounded, bordered card plot.html's own corner panels
 * (Layers, Cameras, Versions) use — reused here rather than invented fresh,
 * so Setup's nav reads as the same kind of surface the rest of the app
 * already put on screen this session. It just sits in the page instead of
 * floating over anything, since there's no map underneath it to keep clear
 * of. Picking an entry swaps which single .panel below it is showing, so
 * Setup reads as one card at a time rather than the whole stack scrolled
 * through. */
/* The nav goes on the RIGHT, so Setup agrees with Assign, Crew and Plot about
   which side the narrow column lives on. Ordered by grid placement rather than
   by moving it in the markup: the nav is still first in the document, which is
   where it belongs for a keyboard and a screen reader — you meet the list of
   sections before the sections. Below the breakpoint it stacks above them,
   which is the same order again. */
.setup-layout { display: grid; grid-template-columns: 1fr 200px; gap: 14px; align-items: start; }
.setup-layout > .setup-nav { grid-column: 2; grid-row: 1; }
.setup-layout > .setup-content { grid-column: 1; grid-row: 1; min-width: 0; }
.setup-nav {
  display: flex; flex-direction: column; gap: 6px;
  /* .panel already sets the background/border/radius/padding; this just
     stops it adding the bottom margin every other .panel wants when it's
     one of several stacked, and pins it in view while the section beside it
     scrolls past a longer list (Crew, Admin settings). */
  margin-bottom: 0;
  position: sticky; top: 12px;
}
/* Breathing room between sections now that they are all on the page at once,
   and a scroll margin so anything that jumps to one by anchor clears the
   app's sticky header the same way scrollToSetupSection does by hand. */
/* The date field the "+ Add day" chip becomes. Sized to the chip it replaces
   so the row does not jump when it opens. */
.chip-date {
  background: none; border: 0; color: var(--text); font: inherit;
  padding: 0; min-width: 130px; outline: none;
}
.chip-date::-webkit-calendar-picker-indicator { filter: invert(1); opacity: .6; cursor: pointer; }

.setup-sec { display: block; scroll-margin-top: 96px; }
.setup-sec + .setup-sec { margin-top: 18px; }

.setup-nav-item {
  display: block; width: 100%; text-align: left;
  padding: 9px 11px; border-radius: 8px;
  /* --panel, not --panel-2: .setup-nav itself is a .panel and already sits at
     --panel-2, so a button matching that shade had nothing but its own 1px
     border for contrast against the strip it's on. Same nesting convention as
     .card inside .panel, one shade lighter than its container. */
  border: 1px solid var(--panel-border); background: var(--panel);
  color: var(--dim); font-size: 13px; font-weight: 600; cursor: pointer;
}
.setup-nav-item:hover { border-color: var(--accent); color: var(--text); }
/* Same accent-border-plus-tint the day tabs and the main tab bar already use
   for "this one's open" — one more place reusing it beats a third slightly
   different way of marking a selection. */
.setup-nav-item.active { border-color: var(--accent); background: #16233a; color: var(--text); }
.setup-content { min-width: 0; }

@media (max-width: 900px) {
  /* A 200px nav column is real width to give up on a phone, and a sticky
     column has nothing to stick beside once it's the full-width thing on
     top — so it drops to a plain horizontal strip, same technique the day
     tabs already use for the same reason. */
  .setup-layout { grid-template-columns: 1fr; }
  /* Stacked, the nav is back above the sections — the columns it was ordered
     into no longer exist. */
  .setup-layout > .setup-nav { grid-column: 1; grid-row: 1; }
  .setup-layout > .setup-content { grid-column: 1; grid-row: 2; }
  .setup-nav {
    position: static;
    flex-direction: row; flex-wrap: nowrap;
    overflow-x: auto; -webkit-overflow-scrolling: touch; scrollbar-width: none;
  }
  .setup-nav::-webkit-scrollbar { display: none; }
  .setup-nav-item { flex: none; }
}

/* Height is set in JS to whatever is actually left below it — a fixed vh cap
   meant a short scroll box with empty screen underneath. */
.sched-scroll {
  overflow-y: auto; overscroll-behavior: contain;
  padding-right: 4px; scroll-behavior: smooth;
}
.scroll-hint { font-size: 11px; color: #5c6472; text-align: center; margin-top: 8px; }

.time-divider {
  margin: 18px 0 8px; padding-bottom: 6px; border-bottom: 1px solid var(--panel-border);
  font-size: 12px; text-transform: uppercase; letter-spacing: 0.05em; color: var(--dim);
}

.set-card { display: flex; gap: 12px; align-items: flex-start; }
.set-card .when {
  flex-shrink: 0; width: 92px; font-variant-numeric: tabular-nums;
}
.set-card .when .start { font-size: 15px; font-weight: 600; }
.set-card .when .end { font-size: 12px; color: var(--dim); }
.set-card .body { flex: 1; min-width: 0; }
.set-card .artist { font-size: 16px; font-weight: 600; }
.set-card .meta { font-size: 12px; color: var(--dim); margin-top: 2px; }
/* the stage rides along with the artist rather than costing a line of its own */
.artist-stage { font-size: 12px; font-weight: 400; color: var(--dim); white-space: nowrap; }
.artist-stage::before { content: "· "; }
.set-card .actions { display: flex; gap: 4px; flex-shrink: 0; }
.set-card.is-past { opacity: 0.5; }
.set-card.status-shot { border-color: #21503c; }
.set-card.status-rolling { border-color: var(--accent); background: #14202f; }
.set-card.status-missed { border-color: var(--bad); }
/* Colour is reserved for things that need attention. A fully covered set gets
   no accent at all — most rows are fine, and they should look it. */
.set-card.uncovered { border-left: 3px solid var(--bad); }
.set-card.partial { border-left: 3px solid var(--warn); }
.set-card.covered { border-left: 3px solid transparent; }
.set-card.status-shot { border-color: var(--panel-border); opacity: 0.75; }
.set-card.status-rolling { border-color: var(--panel-border); background: var(--panel); }
.set-card.status-missed { border-left-color: var(--bad); }

.must-star { color: var(--warn); margin-right: 6px; }
.stage-dot { display: inline-block; width: 7px; height: 7px; border-radius: 50%; vertical-align: middle; margin-right: 5px; }
.restrict-line { color: #d9a06a; }

.stage-tag {
  display: inline-block; font-size: 11px; font-weight: 700; letter-spacing: 0.03em;
  padding: 2px 7px; border-radius: 4px; vertical-align: middle;
  border: 1px solid currentColor;
}
.chip {
  display: inline-flex; align-items: center; gap: 5px; font-size: 12px; font-weight: 500;
  padding: 3px 8px; border-radius: 999px; border: 1px solid var(--panel-border);
  background: transparent; color: var(--text); white-space: nowrap;
}
.chip .dot { width: 7px; height: 7px; border-radius: 50%; flex-shrink: 0; }
.chip.dim { color: var(--dim); }
.stage-toggle { cursor: pointer; user-select: none; }
.stage-toggle.off { color: #5c6472; border-color: #23262f; text-decoration: line-through; }
.chip.add { border-style: dashed; border-color: #262a33; color: #6b7280; cursor: pointer; }
.chip.add:hover { border-color: var(--accent); color: var(--text); }
.day-kind-tag {
  background: none; border: none; color: var(--dim); font: inherit; font-size: 11px;
  font-weight: 800; text-transform: uppercase; letter-spacing: 0.03em; cursor: pointer;
  padding: 0; margin: 0;
}
.day-kind-tag:hover { color: var(--text); }
.day-kind-tag-set { color: var(--accent); }
.chip button { background: none; border: none; color: inherit; padding: 0 0 0 2px; font-size: 12px; cursor: pointer; line-height: 1; }
.chip-row { display: flex; gap: 6px; flex-wrap: wrap; margin-top: 8px; align-items: center; }

.pri {
  font-size: 10px; font-weight: 700; letter-spacing: 0.04em; padding: 2px 6px; border-radius: 4px;
  text-transform: uppercase; vertical-align: middle;
}
.pri.must { background: #4a1520; color: #ff9aa8; }
.pri.high { background: #4a3312; color: #ffca7a; }
.pri.normal { background: #23283180; color: var(--dim); }
.pri.low { background: #23283180; color: var(--dim); }
.pri.skip { background: #232831; color: var(--dim); text-decoration: line-through; }
.pri.blockout { background: #050505; color: #fff; border: 1px solid #2a2e38; }
.pri.blockout button { background: none; border: none; color: inherit; padding: 0 0 0 6px; font-size: 10px; cursor: pointer; }

.tag {
  display: inline-block; font-size: 10px; font-weight: 700; letter-spacing: 0.04em;
  padding: 2px 6px; border-radius: 4px; background: #232831; color: #b9c0d0; text-transform: uppercase;
}
.tag.restrict { background: #4a2a12; color: #ffbe7a; text-transform: none; letter-spacing: 0; font-weight: 600; }
.tag.note { background: #232831; color: #b9c0d0; text-transform: none; letter-spacing: 0; font-weight: 500; }

.status-select { font-size: 12px; padding: 6px 8px; color: var(--dim); background: transparent; border-color: #262a33; }

/* ---- Timeline (time down the page, stages or people across the top) ---- */
/* The grid scrolls inside a fixed window: it opens with now near the top, and
   everything earlier is a scroll away rather than off the end of the page. */
.vtl-scroll {
  overflow-y: auto; overscroll-behavior: contain; touch-action: pan-y;
  border: 1px solid var(--panel-border); border-radius: 12px; background: var(--panel-2);
}
.vtl { background: var(--panel-2); }
/* Step a day at a time from the top bar, either side of the day dropdown —
   the always-visible way to change day, since the grid's own pair below
   scrolls away with the grid. Greyed rather than hidden at the first and
   last day, so the control never changes shape as you move along. */
.day-picker { display: inline-flex; align-items: center; gap: 4px; }
/* How many things this day has to look at — the same count findIssues has
   always produced, said where you pick the day rather than only once you are
   already on it. Warning colour and nothing louder: it is a number to notice
   in passing, not an alarm, and it is not rendered at all at zero, so a clean
   day says nothing rather than saying a reassuring nought. Every OTHER day
   says its count inside its own <option>, in plain text — an option is drawn
   by the OS and cannot be given a colour or a shape here. */
.day-issues, .col-issues {
  flex: none;
  background: var(--warn); color: #17130a; font-weight: 700; font-size: 10px;
  border-radius: 999px; padding: 0 5px; min-width: 16px; text-align: center;
  line-height: 16px; font-variant-numeric: tabular-nums;
}
/* On the crew grid's column headers, beside the name. Sits back a little from
   the day badge: on the header it is one person's share of a day you are
   already looking at, not news about a day you are not. */
.col-issues { margin-left: 2px; }
.day-step { padding: 5px 9px; line-height: 1; }
.day-step:disabled { opacity: 0.35; cursor: default; }
/* The same step, parked at the end of the day's own grid — where you arrive
   having scrolled through everything and the next thing you want is the
   next day. Sits right after the grid itself, so it only comes into view
   once there's nothing left to scroll past. */
.day-nav-buttons {
  display: flex; justify-content: space-between; gap: 10px;
  padding: 14px 4px 6px;
}
.day-nav-buttons button { flex: 0 1 auto; }
.day-nav-buttons span { flex: 0 1 auto; }
/* The part of a set we're actually rolling on: a band washed across the block
   at the point in the set where it falls, in the covering team's colour. A
   closing 30 minutes of a three-hour set sits low in a tall block; an opening
   one sits at the top. With no colour team on it the band is plain grey. */
.vtl-capture {
  position: absolute; left: 0; right: 0;
  border-left: 3px solid transparent;
  z-index: 0; pointer-events: none;
}
/* The window's own times, inside the band — the one thing you can't read off
   the block's position. Used by the simpler, read-only band on the runsheet;
   the planner's own stage grid uses .vtl-band-text below instead. Direct-child
   combinator on purpose: a plain descendant selector here also matched the
   .who/.when spans nested inside .vtl-band-text, forcing them absolute and
   collapsing their shrink-to-fit width to zero (overflow:hidden + ellipsis on
   an auto-width box sizes to its min-content, which an ellipsis-truncatable
   line treats as ~0) — the names silently vanished. */
.vtl-capture > span {
  position: absolute; top: 3px; right: 9px;
  font-size: 11px; font-weight: 600; letter-spacing: 0.01em;
  color: rgba(255, 255, 255, 0.92); font-variant-numeric: tabular-nums;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-width: 90%;
}
/* Who's filming, and when — inside the band itself, since that's where and
   when it's actually happening. Centred rather than pinned to the band's own
   top edge, so it reads from the middle of whatever room the band has — but
   centred in the room the band has LEFT, which is not always the band. The
   block's header sits at the top of the block on a higher z-index layer, and
   a window anchored at the start of its set puts the band directly under it;
   centring in the whole band then printed the crew straight through the
   artist name. --band-text-top / --band-text-bottom are the two insets
   bandTextRoom works out at render time (both 0 when the band is clear), and
   the 3px either side of them are the band's own breathing room — the same
   3px the VTL_BAND_* thresholds in app.js are counted from. overflow:hidden
   is the backstop: a centred column overflows both ways, so a band squeezed
   tighter than its own text has to clip rather than paint back up over the
   header. */
.vtl-band-text {
  position: absolute;
  top: calc(3px + var(--band-text-top, 0px));
  bottom: calc(3px + var(--band-text-bottom, 0px));
  left: 8px; right: 9px;
  display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 1px;
  text-align: center; overflow: hidden;
  pointer-events: none;
}
/* Two or three names in a column this narrow ellipsise — the band's title
   attribute carries the full list, and the panel the block opens spells it
   out properly. */
.vtl-band-text .who {
  font-size: 11px; font-weight: 700; line-height: 13px; color: rgba(255, 255, 255, 0.95);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-width: 100%;
}
.vtl-band-text .when {
  font-size: 10px; font-weight: 500; line-height: 12px; color: rgba(255, 255, 255, 0.7);
  font-variant-numeric: tabular-nums; white-space: nowrap;
}
/* On the stage grid the band itself is the thing you grab — drag it to move
   the window, drag its own bottom edge to change how much of the set it
   covers. The parent block stays put; only the band moves inside it. */
/* The block stays plain grey — the band is what carries the team's colour,
   as a distinct chip sitting inside it (its own inset, its own rounded
   corners, a border all the way round) rather than a wash across the whole
   block. Shared by the stage grid's draggable band and the runsheet's
   read-only one; .grabbable adds the interaction on top. */
.vtl-capture.chip {
  left: 6px; right: 6px; border-radius: 5px;
  border-style: solid; border-width: 1px;
}
/* touch-action stays pan-y until this exact window is the one you've
   picked (.multi-selected, from selectedViaCapture matching it) — that's
   what lets a finger scroll straight through an unpicked band instead of
   the browser reserving the whole gesture for a drag the JS won't even
   start (see onDragStart's own touch check, which agrees with this). */
.vtl-capture.grabbable { pointer-events: auto; cursor: grab; z-index: 2; touch-action: pan-y; }
.vtl-capture.grabbable.multi-selected { touch-action: none; }
.vtl-capture.grabbable:hover { filter: brightness(1.15); }
.vtl-capture.dragging { cursor: grabbing; z-index: 8; box-shadow: 0 8px 22px rgba(0, 0, 0, 0.55); }
.vtl-capture.grabbable .vtl-grip, .vtl-capture.grabbable .vtl-grip-top { z-index: 2; }
/* The set's own span, on its own line right under the artist name — not
   overlaid in the same corner, which is what let a long name's ellipsis
   collide with it. */
.vtl-when {
  font-size: 12px; font-weight: 600; line-height: 14px; color: var(--text); font-variant-numeric: tabular-nums;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis; margin-top: 1px;
}

.vtl-head {
  display: flex; position: sticky; top: 0; z-index: 6;
  box-shadow: 0 1px 0 var(--panel-border);
  background: var(--panel-2); border-bottom: 1px solid var(--panel-border);
}
/* Only shows once a pinch has actually changed the zoom — nothing to reset
   back to otherwise. A child of the header row, but pushed below its
   bottom edge (top: 100%) rather than sitting inside it, so it floats over
   the actual grid instead of crowding a column name — the header doesn't
   clip its overflow, so this stays visible there. Sticks to the right,
   which lands it in the top-right of the grid on the stage grid (no
   Unassigned column to speak of) and just under the Unassigned column on
   the crew grid, since that's always the rightmost one — without needing
   to know any column's exact pixel position, which is what put it inside
   the column cell itself before and threw the header/body alignment off.
   Plain and unfilled on purpose — a quiet "put it back", not something
   that should compete with the column names for attention. */
button.zoom-reset-btn {
  position: absolute; top: 100%; right: 8px; margin-top: 6px; z-index: 10;
  padding: 3px 5px; font-size: 13px; line-height: 1; border-radius: 6px;
  background: var(--panel); border: 1px solid var(--panel-border); color: var(--dim);
}
button.zoom-reset-btn:hover { color: var(--text); border-color: var(--accent); }
/* A flex row rather than a plain text box, so the issue badge on the end keeps
   its width and the NAME is what gets shortened — the other way round, a long
   name ate the badge and the header silently stopped saying anything was
   wrong. The ellipsis moved onto .vtl-col-name with it; on a stage header,
   which has no badge, a single flex child clipping to an ellipsis looks
   exactly like the text box this used to be. */
.vtl-col-head {
  flex: 1; min-width: 0; padding: 9px 10px; font-size: 13px; font-weight: 600;
  border-left: 1px solid var(--panel-border); overflow: hidden;
  display: flex; align-items: center; gap: 4px;
  /* So the badge below can ask how wide its own column is. Safe with flex:1,
     whose basis is 0 — inline-size containment stops a box sizing to its
     contents, and this one never did. */
  container-type: inline-size;
}
.vtl-col-name { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* A column head is as wide as the grid divided by however many people are on
   it: nine crew on a phone is about 28px each, which is one letter of a name
   and no room for anything else. The badge is flex:none, so in that column it
   would win the space and the NAME would be what disappeared — a header that
   says "3" and not whose 3 it is. Below the width where both fit, the badge
   stands down; the day badge in the header is still saying the day has
   something wrong with it, and widening the phone or hiding a stage brings
   the per-person numbers back. */
@container (max-width: 62px) {
  .col-issues { display: none; }
}
/* Once a column is too narrow for a short name it shows the person's
   initials instead. A full name in a 28px column is one letter and an
   ellipsis — which is neither the name nor a deliberate abbreviation. The
   initials are the same ones their avatar wears everywhere else in the
   planner, so the column still says who it is, and the head's title carries
   the full name for a long press. The width is the head's content box, and
   56px is about seven characters at this size: enough for most first names
   whole, and for a longer one to lose its tail and still be readable. Both
   spans are always in the markup; only this rule decides which is on
   screen, so nothing re-renders as the phone rotates. */
.vtl-col-initials { display: none; letter-spacing: .04em; }
@container (max-width: 56px) {
  .vtl-col-name { display: none; }
  .vtl-col-initials { display: inline; }
}
/* The crew grid's own column order is reordered by dragging its header
   left or right. Only real crew columns get this at all (see the reorderable
   check in renderTimelineGrid) — "Unassigned" never gets data-col, so it's
   never a drag target and stays put. */
.vtl-col-head[data-col] { cursor: pointer; touch-action: none; user-select: none; }
.vtl-col-head[data-col].reordering {
  position: relative; z-index: 7; background: var(--panel); cursor: grabbing;
  box-shadow: 0 6px 18px rgba(0, 0, 0, 0.5);
}
.vtl-body { display: flex; position: relative; }
.vtl-gutter {
  flex: 0 0 62px; position: relative;
  background-image: linear-gradient(to bottom, #23262f 1px, transparent 1px);
  background-repeat: repeat-y;
}
.vtl-head .vtl-gutter { flex: 0 0 62px; background: none; }
.vtl-hour {
  position: absolute; left: 0; right: 6px; text-align: right; transform: translateY(-50%);
  font-size: 11px; color: var(--dim); font-variant-numeric: tabular-nums;
}
/* the first label would otherwise sit half above the grid */
.vtl-hour:first-child { transform: translateY(0); }
.vtl-col {
  flex: 1; min-width: 0; position: relative; border-left: 1px solid var(--panel-border);
  background-image: linear-gradient(to bottom, #1c1f27 1px, transparent 1px);
  background-repeat: repeat-y;
  background-size: inherit;
}
.vtl-block {
  position: absolute; left: 4px; right: 4px; border-radius: 7px; overflow: hidden;
  padding: 5px 8px; cursor: pointer;
  background: var(--panel); border: 1px solid #3b4150; border-left: 3px solid #4a5162;
}
.vtl-block:hover { border-color: var(--accent); z-index: 3; }
.vtl-block.covered { border-left-color: #4a5162; }
.vtl-block.partial { border-left-color: var(--warn); }
/* Nothing booked on it yet — reads as empty, not as an emergency. */
.vtl-block.uncovered {
  background: #16181d; border-color: #23262f; border-left-color: #3a3f4b;
  color: var(--dim);
}
.vtl-block.uncovered .vtl-artist { color: #9aa1b1; font-weight: 500; }
/* A whole set reading as one flat, team-tinted rectangle made it impossible
   to tell the booked set apart from the moment actually being filmed inside
   it. A quiet diagonal hatch now runs behind the whole set — "happening,
   not necessarily on camera" — and the capture band cuts a clear, solid
   rectangle out of it, so what's actually rolling pops out at a glance. */
.vtl-block.stage-set {
  background-color: var(--panel);
  background-image: repeating-linear-gradient(135deg, rgba(255, 255, 255, 0.05) 0 2px, transparent 2px 10px);
  /* The band sits inset only 6px from the block's edge — the block's own
     8px padding left barely any breathing room between its left border and
     the first letter of the artist name. A little extra here just for the
     stage grid, where that band exists. */
  padding-left: 13px;
}
.vtl-block.stage-set.uncovered {
  background-color: #16181d;
  background-image: repeating-linear-gradient(135deg, rgba(255, 255, 255, 0.03) 0 2px, transparent 2px 10px);
}
/* The Assign page nests a capture band inside every set block, each with its
   own border — the crew grid's heavier left accent (useful there to read a
   kind's colour at a glance) just adds another edge to an already busy box.
   Matching it to the other three sides here keeps the colour cue without
   the extra weight. */
.vtl-block.stage-set { border-left-width: 1px; }
/* Marked to record in full rather than a chosen window — worth standing out
   from every other set on the grid at a glance, in both places it shows up,
   regardless of coverage state underneath it. */
.vtl-block.full-set {
  border-color: var(--pink); background: rgba(255, 45, 149, 0.14);
}
.vtl-block.full-set.stage-set {
  background-color: rgba(255, 45, 149, 0.1);
  background-image: repeating-linear-gradient(135deg, rgba(255, 45, 149, 0.18) 0 2px, transparent 2px 10px);
}
/* Meal is the default purple already used everywhere else for "off camera".
   Meeting/setup get their own colour since they're a different kind of thing
   to notice at a glance — not a break, still booked out. Blockout reads as
   "closed" rather than coloured at all, since it isn't a kind of activity. */
.vtl-block.is-break, .vtl-block.kind-meal { background: #1c1730; border-color: var(--purple); border-left-color: var(--purple); }
.vtl-block.kind-meeting, .vtl-block.kind-setup { background: #2a1d10; border-color: var(--orange); border-left-color: var(--orange); }
.vtl-block.kind-editing { background: #10261c; border-color: var(--good); border-left-color: var(--good); }
/* Two attributes any kind can be given from Setup — a quiet diagonal texture,
   or the same "needs attention" flash used for uncovered sets/crew slots.
   Both are added as plain classes at render time (blockKindStyle in app.js
   decides which kinds get them by default), so they layer on top of
   whatever colour the block already has, custom or not. */
.vtl-block.kind-hatch, .vtl-capture.kind-hatch { background-image: repeating-linear-gradient(135deg, rgba(255, 255, 255, 0.06) 0 2px, transparent 2px 10px); }
/* A bolder border — same colour the kind already has, just heavier, for a
   kind that should stand out by outline alone rather than needing its own
   colour too (or on top of one, for even more weight). A capture band gets
   its own border-width straight from its inline style instead (it's driven
   by a team, not a kind), so it isn't part of this rule. */
.vtl-block.kind-border { border-width: 2px; }
/* Unlike the uncovered-slot flash, a kind (or team) can already have a real
   colour of its own — fading its background to transparent and back would
   just make it blink out rather than flash, so this pulses an inset glow
   over whatever's already there instead of replacing the background. */
@keyframes kind-flash-glow { 0%, 100% { box-shadow: none; } 50% { box-shadow: 0 0 0 2px var(--warn-strong) inset; } }
.vtl-block.kind-flash, .vtl-capture.kind-flash { animation: flash-uncovered-border 2.6s ease-in-out infinite, kind-flash-glow 2.6s ease-in-out infinite; }
.vtl-block.kind-flash .vtl-artist, .vtl-block.kind-flash .vtl-sub { animation: flash-uncovered-text 2.6s ease-in-out infinite; }
@media (prefers-reduced-motion: reduce) {
  .vtl-block.kind-flash, .vtl-block.kind-flash .vtl-artist, .vtl-block.kind-flash .vtl-sub,
  .vtl-capture.kind-flash { animation: none; }
}
.vtl-block.kind-dump { background: #0f2626; border-color: var(--teal); border-left-color: var(--teal); }
.vtl-block.kind-blockout {
  background-color: #050505;
  background-image: repeating-linear-gradient(135deg, rgba(255,255,255,0.05) 0 2px, transparent 2px 10px);
  border-color: #000; border-left-color: #000;
}
.vtl-block.drag-preview { opacity: 0.8; pointer-events: none; z-index: 6; box-shadow: 0 6px 18px rgba(0,0,0,0.5); }
.vtl-block.gone { opacity: 0.35; filter: grayscale(1); }
.vtl-block.live { box-shadow: 0 0 0 2px rgba(79,141,253,0.35); }
.vtl-block.not-filming { background: #14161b; border-color: #23262f; border-left-color: #2a2e38; opacity: 0.6; }
.vtl-restrict { font-size: 11px; color: #d9a06a; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; margin-top: 2px; }
.vtl-block > .vtl-artist, .vtl-block > .vtl-when, .vtl-block > .vtl-sub, .vtl-block > .vtl-restrict { position: relative; z-index: 1; }
/* The stage grid's header sits visually above the band even when the band
   runs flush with the block's own top — a capture starting at the very
   start of the set reads that way, band and all, rather than being nudged
   down to make room. pointer-events:none lets a click or drag starting on
   the header text fall straight through to whatever's actually there
   underneath (the band, and its grip) rather than the header eating it. */
.vtl-block.stage-set .vtl-artist, .vtl-block.stage-set .vtl-when {
  z-index: 3; pointer-events: none;
}
/* Fixed line boxes, not the font's own "normal" — app.js's VTL_HEADER_*
   figures are these numbers plus the block's padding, and blockTextMode
   hands out lines by counting them. A line box that quietly changed height
   with the font would leave that arithmetic wrong and the band's text back
   under the artist name. 5 + 17 + 1 + 14 = 37, inside the 38 reserved. */
.vtl-artist { font-size: 14px; font-weight: 700; line-height: 17px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.vtl-sub { font-size: 11px; line-height: 14px; color: var(--dim); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; margin-top: 1px; }
/* Too short for two lines: the artist alone, a size down, tight to the top —
   2 + 14 = 16, the VTL_HEADER_COMPACT_PX the band's own room is measured
   against. Shorter still and the block carries no text at all, just its
   title attribute; there is nothing to reserve, so the band gets the lot. */
.vtl-block.text-compact { padding-top: 2px; padding-bottom: 2px; }
.vtl-block.text-compact .vtl-artist { font-size: 12px; line-height: 14px; }
.vtl-block.text-none { padding-top: 0; padding-bottom: 0; }
/* Catering hours, washed across every crew column at once — a break outside
   them then reads as obviously outside them. */
.vtl-catering {
  position: absolute; left: 62px; right: 0; z-index: 0; pointer-events: none;
  background: rgba(124, 92, 255, 0.05);
  border-top: 1px solid rgba(124, 92, 255, 0.18);
  border-bottom: 1px solid rgba(124, 92, 255, 0.18);
}
.vtl-catering span {
  position: absolute; right: 8px; top: 3px;
  font-size: 10px; letter-spacing: 0.04em; text-transform: uppercase; color: rgba(169, 146, 255, 0.55);
}
.vtl-now { position: absolute; left: 62px; right: 0; height: 2px; background: var(--bad); z-index: 4; pointer-events: none; }
.vtl-now::before {
  content: ""; position: absolute; left: -4px; top: -3px; width: 8px; height: 8px;
  border-radius: 50%; background: var(--bad);
}
/* Per-person reference lines on the crew grid: when their day actually
   starts, and where it runs past a standard-length day for them. Dashed and
   quiet rather than solid like "now" — a marker to read against, not
   something happening. The label sits right on the line rather than beside
   it, since a crew column is rarely wide enough to spare the margin. */
.vtl-daymark { position: absolute; left: 0; right: 0; height: 0; z-index: 1; pointer-events: none; }
.vtl-daymark::after {
  content: attr(data-label); position: absolute; left: 2px; top: -1px; transform: translateY(-100%);
  font-size: 9px; letter-spacing: 0.02em; white-space: nowrap; line-height: 1.4;
}
/* Solid and grabbable, like .vtl-now, rather than the quiet dashed treatment
   other daymarks get — this is the one line on the grid you can drag, so it
   needs to read as such. The generous top offset keeps the actual line thin
   (visually matching .vtl-now) while giving the drag handle a comfortable
   hit target without it looking oversized. */
.vtl-daymark.call {
  border-top: none; height: 2px; margin-top: -1px;
  background: var(--warn); z-index: 4; pointer-events: auto; cursor: ns-resize;
}
.vtl-daymark.call::before {
  content: ""; position: absolute; left: -4px; top: -3px; width: 8px; height: 8px;
  border-radius: 50%; background: var(--warn);
}
.vtl-daymark.call::after { color: var(--warn); font-weight: 600; }
.vtl-daymark.call.dragging { cursor: grabbing; }
.vtl-daymark.standard { border-top: 1px dashed rgba(255, 92, 92, 0.55); }
.vtl-daymark.standard::after { top: auto; bottom: -1px; transform: translateY(100%); color: rgba(255, 92, 92, 0.9); }
.tl-legend { display: flex; gap: 14px; flex-wrap: wrap; font-size: 12px; color: var(--dim); margin-top: 10px; }

/* ---- Assign workspace: the grid, with the crew beside it ---- */
/* The grid keeps the width it had on its own; the panel hangs off the side of
   the reading column into whatever margin the window has, up to its full width.
   Below that it just sits inside, as before. */
.assign-layout { display: grid; grid-template-columns: 1fr 320px; gap: 14px; align-items: start; }
@media (min-width: 1140px) {
  .assign-layout { margin-right: calc(-1 * clamp(0px, (100vw - 1132px) / 2, 334px)); }
}
/* Full width: the grid and its panel span the window together. */
.assign-layout.bleed-layout {
  width: 100vw; margin-left: calc(50% - 50vw); margin-right: calc(50% - 50vw);
  padding: 0 16px;
}
.assign-side {
  background: var(--panel-2); border: 1px solid var(--panel-border); border-radius: 12px;
  padding: 14px; overflow-y: auto;
  /* stays put while the page scrolls — it's the thing you're working with */
  position: sticky; top: 12px; align-self: start;
}
.assign-empty { color: var(--dim); font-size: 13px; margin-bottom: 16px; line-height: 1.5; }
.assign-head { border-bottom: 1px solid var(--panel-border); padding-bottom: 12px; }
.assign-artist { font-size: 17px; font-weight: 700; line-height: 1.2; }
.assign-meta { font-size: 12px; color: var(--dim); margin-top: 3px; }
.assign-restrict { font-size: 12px; color: #d9a06a; margin-top: 5px; }

.assign-crew {
  display: flex; align-items: center; gap: 10px; padding: 8px 10px; margin-bottom: 6px;
  border: 1px solid var(--panel-border); border-radius: 9px; cursor: pointer; background: var(--panel);
}
.assign-crew:hover { border-color: var(--accent); }
.assign-crew.on { border-color: var(--good); background: #13251d; }
.assign-crew.busy { opacity: 0.5; }
.assign-crew .who { flex: 1; min-width: 0; }
.assign-mark { color: var(--dim); font-weight: 700; }
.assign-crew.on .assign-mark { color: var(--good); }
.assign-crew[data-action="spotlight-crew"],
.assign-crew[data-action="assign-toggle"],
.assign-crew[data-action="team-drag-source"] { cursor: grab; touch-action: none; }
.assign-crew[data-action="spotlight-crew"]:active,
.assign-crew[data-action="assign-toggle"]:active,
.assign-crew[data-action="team-drag-source"]:active { cursor: grabbing; }

/* Dragging a name off the Available crew list and onto a set on the grid. */
.crew-drag-ghost {
  position: fixed; z-index: 50; pointer-events: none;
  padding: 7px 12px; font-size: 12px; font-weight: 600; border-radius: 8px;
  color: #fff; box-shadow: 0 8px 22px rgba(0, 0, 0, 0.5);
}
/* The set that name would land on if let go right now — sticky in the sense
   that it stays lit even once the pointer's drifted a little off the block
   itself, not just while sitting exactly on top of it. */
.vtl-block.assign-drop-target {
  outline: 2px solid var(--good); outline-offset: 1px; z-index: 5;
}
/* Hovering directly over an existing shoot window while dragging someone
   onto it — this is the specific window they'd join, not just "somewhere on
   this set", so it gets its own lit-up outline rather than relying on the
   block's own outline to say which window that'll be. */
.vtl-capture.assign-drop-target-band {
  outline: 2px solid var(--good); outline-offset: 1px; z-index: 7;
  background-color: rgba(62, 207, 142, 0.16);
}
/* Dragging a name onto a set with no shoot window yet: rather than just
   dumping them on some fixed default stretch, this previews exactly where
   the new window would land as you move up and down over the block, before
   you've let go of anything. */
.vtl-capture-preview {
  position: absolute; left: 6px; right: 6px; z-index: 6; pointer-events: none;
  border-radius: 5px; border: 1.5px dashed var(--good);
  background: rgba(62, 207, 142, 0.16);
  display: flex; align-items: center; justify-content: center;
  font-size: 10px; font-weight: 600; color: var(--good);
  white-space: nowrap; overflow: hidden;
}

/* the set you're working on */
.vtl-block.picked { outline: 2px solid var(--accent); outline-offset: 1px; z-index: 4; }
/* The crew grid's own equivalent of .picked — sits above the block-menu's
   dark backdrop (z-index 39) rather than behind it, so the block you tapped
   stays fully lit while everything else dims. */
.vtl-block.menu-active { outline: 2px solid var(--accent); outline-offset: 1px; z-index: 41; }
/* Everything but the picked block and the panel dims — the picked block's
   own z-index (4) already keeps it, and a hovered block (3), above this. */
.vtl-spotlight { position: absolute; inset: 0; background: rgba(0, 0, 0, 0.6); z-index: 2; pointer-events: none; }
/* Picking a name instead of a set can light up several blocks at once —
   everywhere that person is booked — rather than just one, so each match
   gets lifted above the dim overlay the same way the picked set already is,
   instead of punching one single hole in it. Only the part that's actually
   theirs stays lit, though: the hatch (the rest of the set, not on camera)
   gets its own dimming right on the block, sitting below the band (z:2) and
   the header (z:3) so those two stay fully bright regardless. */
.vtl-block.lit { z-index: 4; }
.vtl-block.lit::before { content: ""; position: absolute; inset: 0; z-index: 0; background: rgba(0, 0, 0, 0.6); }

/* ---- Dragging on the crew grid ----
   Empty space makes a break, a block moves or resizes, and the set's full
   extent shows as a dashed outline while you drag its capture window. */
.vtl-col[data-crew] { cursor: crosshair; }
/* Same deal as the stage grid's capture band: touch-action only turns off
   once a block is the one you've actually picked (.menu-active, from
   blockMenu pointing at it) — onDragStart's own touch check makes tapping
   an unpicked block open its menu rather than drag it, so scrolling stays
   available right up until that point. */
.vtl-col[data-crew] .vtl-block { cursor: grab; touch-action: pan-y; }
.vtl-col[data-crew] .vtl-block.menu-active,
.vtl-col[data-crew] .vtl-block.multi-selected { touch-action: none; }
/* The touch-select tool armed (body.touch-drag-armed): the very next touch
   is a Ctrl/Cmd-drag stand-in — a block toggles into the selection outright
   and empty space starts the same box-select rectangle a mouse would drag
   out, rather than either one just scrolling past. */
body.touch-drag-armed .vtl-col[data-crew] .vtl-block,
body.touch-drag-armed .vtl-col[data-crew] { touch-action: none; }
/* Nobody's shooting these — a slow fade between the normal dim grey and a
   warm orange, across the name, the stage/length line, and the block's own
   border, keeps them nagging at you without being an alarm. */
@keyframes flash-uncovered-text { 0%, 100% { color: inherit; } 50% { color: var(--warn-strong); } }
@keyframes flash-uncovered-border {
  0%, 100% { border-color: #3b4150; border-left-color: #4a5162; }
  50% { border-color: var(--warn-strong); border-left-color: var(--warn-strong); }
}
/* The same fade, behind a quiet diagonal hatch — same "not covered" language
   as a blockout, just orange instead of red since nobody's actually stuck,
   there's just nobody on it yet. */
@keyframes flash-uncovered-bg {
  0%, 100% { background-color: transparent; }
  50% { background-color: rgba(255, 122, 26, 0.18); }
}
.vtl-col[data-crew="__none__"] .vtl-block {
  background-image: repeating-linear-gradient(135deg, rgba(255, 255, 255, 0.05) 0 8px, transparent 8px 16px);
  animation: flash-uncovered-border 2.6s ease-in-out infinite, flash-uncovered-bg 2.6s ease-in-out infinite;
}
.vtl-col[data-crew="__none__"] .vtl-block .vtl-artist,
.vtl-col[data-crew="__none__"] .vtl-block .vtl-sub { animation: flash-uncovered-text 2.6s ease-in-out infinite; }
/* Same nag on the Assign page — a set someone actually needs to be on,
   with nobody assigned yet, ought to be exactly as hard to miss as an
   uncovered crew slot is on the other page. */
.vtl-block.stage-set.uncovered {
  animation: flash-uncovered-border 2.6s ease-in-out infinite, flash-uncovered-bg 2.6s ease-in-out infinite;
}
.vtl-block.stage-set.uncovered .vtl-artist,
.vtl-block.stage-set.uncovered .vtl-when { animation: flash-uncovered-text 2.6s ease-in-out infinite; }
/* Same nag again, one level down — a single window nobody's on yet, inside
   a set that otherwise has other, covered windows (so the outer block
   itself doesn't qualify as "uncovered" — only this one window does). */
.vtl-capture.uncovered {
  animation: flash-uncovered-border 2.6s ease-in-out infinite, flash-uncovered-bg 2.6s ease-in-out infinite;
}
.vtl-capture.uncovered .vtl-band-text { animation: flash-uncovered-text 2.6s ease-in-out infinite; }
/* Someone on this window is also booked on another shoot window that
   overlaps it — live while it's actually being dragged, and it keeps going
   after you let go too, for as long as the double-booking is still really
   there (see windowHasConflict). Fast and red on purpose — this is "you're
   double-booking someone", not the slow orange "nobody's on it yet" nag.
   A team or kind can already have its own real colour here, set inline
   (background-color/border-color), which a same-property animation would
   just get silently overridden by — so this pulses a box-shadow glow on
   top of whatever's already there instead, the same trick kind-flash-glow
   already uses for exactly that reason. */
@keyframes conflict-flash-glow {
  0%, 100% { box-shadow: 0 0 0 2px var(--bad) inset; }
  50% { box-shadow: 0 0 0 3px #ffd0d0 inset, 0 0 12px 2px rgba(255, 92, 92, 0.65); }
}
/* The wash itself is a pseudo-element, not background-color on the block —
   same inline-override problem as the border, and a pseudo-element sidesteps
   it entirely rather than fighting the block's own inline style for the
   property. */
@keyframes conflict-flash-bg { 0%, 100% { opacity: 0; } 50% { opacity: 1; } }
.vtl-capture.conflict-flash, .vtl-block.conflict-flash {
  animation: conflict-flash-glow 0.4s ease-in-out infinite;
  z-index: 9;
}
.vtl-capture.conflict-flash::after, .vtl-block.conflict-flash::after {
  content: ""; position: absolute; inset: 0; border-radius: inherit;
  background: rgba(255, 92, 92, 0.35); pointer-events: none;
  animation: conflict-flash-bg 0.4s ease-in-out infinite;
}
@media (prefers-reduced-motion: reduce) {
  .vtl-col[data-crew="__none__"] .vtl-block,
  .vtl-col[data-crew="__none__"] .vtl-block .vtl-artist,
  .vtl-col[data-crew="__none__"] .vtl-block .vtl-sub,
  .vtl-block.stage-set.uncovered,
  .vtl-block.stage-set.uncovered .vtl-artist,
  .vtl-block.stage-set.uncovered .vtl-when,
  .vtl-capture.uncovered, .vtl-capture.uncovered .vtl-band-text,
  .vtl-capture.conflict-flash, .vtl-block.conflict-flash { animation: none; box-shadow: 0 0 0 2px var(--bad) inset; }
  .vtl-capture.conflict-flash::after, .vtl-block.conflict-flash::after { animation: none; opacity: 0.5; }
}
/* Reusable version of the same nag, for anywhere else something's worth
   flagging without being an alarm — a call time booked in too late, say. */
.flash-uncovered-text { animation: flash-uncovered-text 2.6s ease-in-out infinite; }
@media (prefers-reduced-motion: reduce) {
  .flash-uncovered-text { animation: none; color: var(--warn-strong); }
}
/* Marked as "not shooting" — reads as switched off, not as a problem to fix. */
.vtl-block.stage-set.skip {
  background-color: #14161b; background-image: none;
  border-color: #23262f; color: var(--dim); opacity: 0.65;
}
.vtl-block.stage-set.skip .vtl-artist { color: #9aa1b1; font-weight: 500; }
.vtl-block.dragging {
  cursor: grabbing; z-index: 8;
  box-shadow: 0 8px 22px rgba(0, 0, 0, 0.55);
  border-color: var(--text);
}
.vtl-grip { position: absolute; left: 0; right: 0; bottom: 0; height: 9px; cursor: ns-resize; z-index: 2; }
.vtl-grip::after {
  content: ""; position: absolute; left: 50%; bottom: 2px; margin-left: -11px;
  width: 22px; height: 2px; border-radius: 2px; background: rgba(255, 255, 255, 0.28);
}
.vtl-block:hover .vtl-grip::after { background: rgba(255, 255, 255, 0.6); }
/* Same handle, mirrored to the top — grab it to extend or shorten from the
   start instead of the end. */
.vtl-grip-top { position: absolute; left: 0; right: 0; top: 0; height: 9px; cursor: ns-resize; z-index: 2; }
.vtl-grip-top::after {
  content: ""; position: absolute; left: 50%; top: 2px; margin-left: -11px;
  width: 22px; height: 2px; border-radius: 2px; background: rgba(255, 255, 255, 0.28);
}
.vtl-block:hover .vtl-grip-top::after { background: rgba(255, 255, 255, 0.6); }
.vtl-ghost {
  position: absolute; left: 2px; right: 2px; z-index: 5; pointer-events: none;
  border: 1px dashed rgba(255, 255, 255, 0.45); border-radius: 7px;
  background: rgba(255, 255, 255, 0.04);
}
.vtl-col.drop-target { background-color: rgba(79, 141, 253, 0.09); box-shadow: inset 0 0 0 1px rgba(79, 141, 253, 0.5); }
/* Alt held mid-drag: dropping here adds them alongside whoever's already on
   it, rather than handing it over — green instead of blue so that's clear
   before you let go. */
.vtl-col.drop-target.copy { background-color: rgba(62, 207, 142, 0.12); box-shadow: inset 0 0 0 1px rgba(62, 207, 142, 0.6); }
body.dragging-alt, body.dragging-alt * { cursor: copy !important; }
/* Sits where the block started for as long as Alt is held, so the one
 * following the cursor reads as a new copy rather than the original moving. */
.alt-origin-ghost {
  opacity: 0.45; pointer-events: none; outline: 1px dashed rgba(255,255,255,0.5); outline-offset: -1px;
}
/* The other columns' copies of a shared set, lit up while you're touching
 * one of them — a heads-up that they're all coming along together. */
.linked-highlight { outline: 2px solid var(--accent); outline-offset: 1px; z-index: 3; }
/* Ctrl/Cmd-click picks a block for a group move — dashed rather than solid
 * so it doesn't get confused with the block-menu's own solid highlight. */
.multi-selected { outline: 2px dashed var(--good); outline-offset: 1px; z-index: 3; }
/* Plain drag over empty space to box-select — the rectangle itself, and
   a preview on whatever it's currently touching, before you've let go. */
.box-select-rect {
  position: fixed; z-index: 45; pointer-events: none;
  background: rgba(62, 207, 142, 0.12); border: 1px solid var(--good);
}
.box-hover { outline: 2px dashed var(--good); outline-offset: 1px; z-index: 3; }

/* What you tapped, and what you can do with it — the first section of the
   Crew page's side panel rather than a popup over the grid, so the block
   stays fully readable while it's open instead of something sitting on top
   of it. This is the one thing in the panel that appears and disappears on
   its own rather than always being there, so it gets a slow beat to read as
   arriving/leaving rather than just popping. It opens by growing its own
   height, which pushes the rest of the panel down with it, and closes by
   reversing that — see closeBlockMenu() in planner.html, which holds the
   actual removal until this finishes instead of cutting it off mid-flight. */
/* The wrapper is what makes the rest of the panel move. scaleY only paints
   smaller, it doesn't take up less room, so animating the menu alone left
   everything under it jumping to its final place on the first frame and
   waiting there. A grid track measured in fr IS animatable, so collapsing
   0fr -> 1fr changes real layout height and Blocks, Teams, Catering and the
   crew list slide along with it. The bottom margin animates too — .panel's
   own 14px would otherwise stay behind as a gap after the menu had gone.

   .block-menu-clip exists because the track can only collapse as far as its
   item's own minimum, and min-height:0 only frees the CONTENT box: .panel's
   14px of padding and 1px of border still occupy space, so animating the
   menu directly stalled 30px short and then snapped shut when the removal
   timer fired. The clip layer carries no padding, border or background of
   its own, so it really can reach zero; the panel keeps its chrome and is
   clipped inside it. */
.block-menu-slot {
  display: grid; grid-template-rows: 1fr; margin-bottom: 14px;
  animation: block-menu-slot-in 0.9s cubic-bezier(0.2, 0.8, 0.2, 1);
}
.block-menu-slot.closing {
  animation: block-menu-slot-out 0.9s cubic-bezier(0.2, 0.8, 0.2, 1) forwards;
}
@keyframes block-menu-slot-in {
  from { grid-template-rows: 0fr; margin-bottom: 0; }
  to { grid-template-rows: 1fr; margin-bottom: 14px; }
}
@keyframes block-menu-slot-out {
  from { grid-template-rows: 1fr; margin-bottom: 14px; }
  to { grid-template-rows: 0fr; margin-bottom: 0; }
}
.block-menu-clip { min-height: 0; overflow: hidden; }
/* The panel itself only fades now. It used to carry the whole animation as
   a transform; with the slot handling the movement, a scaleY on top of it
   reads as the thing being squashed twice over. */
.block-menu {
  display: flex; flex-direction: column; gap: 6px;
  margin-bottom: 0;
  animation: block-menu-in 0.9s cubic-bezier(0.2, 0.8, 0.2, 1);
}
.block-menu.closing {
  animation: block-menu-out 0.9s cubic-bezier(0.2, 0.8, 0.2, 1) forwards;
}
@keyframes block-menu-in {
  from { opacity: 0; }
  to { opacity: 1; }
}
@keyframes block-menu-out {
  from { opacity: 1; }
  to { opacity: 0; }
}
.bm-head { display: flex; justify-content: space-between; align-items: flex-start; gap: 8px; }
.bm-head-title { font-size: 14px; font-weight: 700; display: flex; gap: 8px; align-items: baseline; flex: 1; min-width: 0; }
.bm-head-title span { font-size: 11px; font-weight: 500; color: var(--dim); font-variant-numeric: tabular-nums; white-space: nowrap; }
.block-menu-close { flex-shrink: 0; margin-top: -2px; }
.bm-sub { font-size: 12px; color: var(--dim); margin-bottom: 3px; }
.bm-label { font-size: 11px; text-transform: uppercase; letter-spacing: 0.04em; color: var(--dim); margin-top: 4px; }
.bm-people { display: flex; flex-wrap: wrap; gap: 5px; }
.bm-people button { flex: 0 0 auto; }
.bm-people button.is-busy { opacity: 0.45; }
/* The kind buttons reuse .palette-chip so they show the same colour as
   everywhere else that kind appears — grabbing isn't a thing here though,
   just a tap. */
.bm-people .palette-chip { cursor: pointer; font-family: inherit; touch-action: auto; }
.bm-people .bm-suggest {
  flex: 1 1 100%; display: flex; flex-direction: column; align-items: flex-start; gap: 1px;
  text-align: left; white-space: normal; border-color: var(--accent);
}
.bm-people .bm-suggest span { font-size: 11px; font-weight: 500; color: var(--dim); }
.bm-custom-input {
  width: 100%; margin: 4px 0 6px; padding: 6px 8px; font-size: 13px;
  background: var(--panel); border: 1px solid var(--panel-border); border-radius: 7px; color: var(--text);
}
.swatch {
  width: 26px; height: 26px; border-radius: 999px; border: 2px solid var(--panel-border);
  cursor: pointer; padding: 0;
}
.swatch:hover { border-color: var(--text); }
/* The one non-colour swatch — a hazard pattern for flagging something as a
   warning, same stripe language as a blockout or the danger zone. */
.swatch-stripe {
  background-color: #050505;
  background-image: repeating-linear-gradient(135deg, var(--bad) 0 4px, #050505 4px 8px);
}
.vtl-block.custom-stripe {
  background-color: #0a0505;
  background-image: repeating-linear-gradient(135deg, rgba(255, 92, 92, 0.35) 0 6px, #0a0505 6px 12px);
  border-color: var(--bad); border-left-color: var(--bad); color: #fff;
}
.block-menu button.bad { color: var(--bad); border-color: #3a2429; }
.block-menu button.bad:hover { border-color: var(--bad); }

/* Catering hours, editable where you're using them. */
.cater-row { display: flex; align-items: center; gap: 6px; margin-bottom: 5px; }
.cater-row input[type="time"] {
  flex: 1; min-width: 0; padding: 5px 7px; font-size: 12px;
  background: var(--panel); color: var(--text);
  border: 1px solid var(--panel-border); border-radius: 7px;
}
.cater-row span { color: var(--dim); font-size: 12px; }

/* While the band is being dragged it's lifted out of its block (which clips
   overflow) to sit directly in the column instead — same as how a crew-grid
   block already isn't nested in anything that clips it. */

/* A thin line at whatever time you've snapped to — another set or break
   starting or ending nearby. */
.vtl-snap-line {
  position: absolute; left: 0; right: 0; height: 0; z-index: 9;
  border-top: 1px dashed var(--text); opacity: 0; pointer-events: none;
}
.vtl-snap-line.active { opacity: 0.55; }
/* The actual block whose edge the guide line is matching — the line alone
   spans every column, so without this it's a guess which one it really
   means. */
.vtl-block.snap-target { outline: 2px solid var(--text); outline-offset: 1px; z-index: 4; }

.vtl-ghost span {
  position: absolute; top: 3px; left: 7px;
  font-size: 10px; color: rgba(255, 255, 255, 0.65); font-variant-numeric: tabular-nums;
}

/* Someone else's capture-window drag, still in progress on their screen —
   see updateRemoteDragGhosts(). Same shape as .vtl-ghost (this tab's own
   in-progress drag) so it reads as "a block being moved", just coloured to
   whoever's actually doing it and never draggable itself. */
.remote-drag-ghost {
  position: absolute; left: 2px; right: 2px; z-index: 5; pointer-events: none;
  border: 2px dashed; border-radius: 7px;
  background: rgba(255, 255, 255, 0.06);
  transition: top 80ms linear, height 80ms linear;
}
.remote-drag-ghost span {
  position: absolute; top: -18px; left: 0;
  font-size: 10px; font-weight: 700; color: #0f1115;
  padding: 1px 6px; border-radius: 999px; white-space: nowrap;
}
body.dragging-grid, body.dragging-grid * { user-select: none !important; }

/* Breaks panel on the Crew view — same shape as the assign list. */
.brk-row {
  display: flex; align-items: flex-start; gap: 10px; padding: 8px 10px; margin-bottom: 6px;
  border: 1px solid var(--panel-border); border-radius: 9px; background: var(--panel);
}
.brk-row .who { flex: 1; min-width: 0; }
.brk-row .chip-row { display: flex; flex-wrap: wrap; gap: 5px; }
.brk-row .chip { cursor: pointer; }
.brk-row.open { border-color: var(--accent); }
/* Click anywhere on the row to reveal a few more settings inline — delete
   included — instead of a separate edit icon and a modal for it. The chevron
   is just a state hint now, not its own button; the whole row is the target. */
.accordion-chevron {
  font-size: 10px; color: var(--dim); flex-shrink: 0;
  transition: transform 0.15s ease;
}
.accordion-chevron.open { transform: rotate(180deg); }
.crew-accordion {
  margin-top: 8px; padding-top: 8px; border-top: 1px solid var(--panel-border);
  animation: accordion-in 0.5s cubic-bezier(0.2, 0.8, 0.2, 1);
  transform-origin: top;
}
@keyframes accordion-in {
  from { opacity: 0; transform: translateY(-10px) scaleY(0.85); }
  to { opacity: 1; transform: translateY(0) scaleY(1); }
}
.crew-accordion .field-block { margin-bottom: 0; }
.crew-accordion .field input { font-size: 12px; padding: 5px 7px; }
/* A call time that's been set by hand rather than worked out from their day —
   worth a glance of colour so it reads as a deliberate choice, not the plan
   quietly disagreeing with itself. */
.crew-accordion .field input.call-override {
  border-color: var(--warn); background: rgba(230, 179, 60, 0.12); color: var(--warn);
}
.crew-accordion .swatch { width: 20px; height: 20px; }
/* A mini workload readout per crew member: filming time, call, and when
   they're done — the three things worth knowing about their day at a glance. */
.crew-stats { display: flex; flex-wrap: wrap; align-items: center; gap: 3px 10px; font-size: 12px; color: var(--dim); }
.crew-stats b { color: var(--text); font-weight: 600; }
.crew-stats b.bad { color: var(--bad); }
.crew-stats b.call-override-text { color: var(--warn); }
.crew-stats input[type="time"] {
  padding: 1px 4px; font-size: 12px; font-weight: 600; font-family: inherit;
  background: transparent; color: var(--text);
  border: 1px solid var(--panel-border); border-radius: 5px;
}

/* The drag-in palette: pick a kind of time up, carry it to a column, drop it.
   Each chip is coloured the same as the block it becomes on the grid, so the
   kind is recognisable before you've even dragged it. */
.palette-row { display: flex; flex-wrap: wrap; gap: 6px; }
.palette-chip {
  padding: 7px 12px; font-size: 12px; font-weight: 600; border-radius: 8px;
  background: var(--panel); border: 1px solid var(--panel-border); color: var(--text);
  cursor: grab; touch-action: none; user-select: none;
}
.palette-chip:active { cursor: grabbing; }
/* A plain reference, not a real chip — same shape and size as the ones
   beside it, but nothing to drag or tap. Just here to say what a pink
   border means wherever it shows up on the grid. */
.palette-chip-ref {
  padding: 7px 12px; font-size: 12px; font-weight: 600; border-radius: 8px;
  border: 1.5px solid #ff2d95; background: rgba(255, 45, 149, 0.14); color: var(--text);
  cursor: default; user-select: none;
}
.palette-chip.kind-meal { background: #1c1730; border-color: var(--purple); }
.palette-chip.kind-meeting, .palette-chip.kind-setup { background: #2a1d10; border-color: var(--orange); }
.palette-chip.kind-editing { background: #10261c; border-color: var(--good); }
.palette-chip.kind-dump { background: #0f2626; border-color: var(--teal); }
.palette-chip.kind-blockout {
  background-color: #050505;
  background-image: repeating-linear-gradient(135deg, rgba(255,255,255,0.08) 0 2px, transparent 2px 10px);
  border-color: #55585f; color: #ddd;
}
.palette-ghost {
  position: fixed; z-index: 50; pointer-events: none;
  padding: 7px 12px; font-size: 12px; font-weight: 600; border-radius: 8px;
  background: var(--purple); color: #fff; box-shadow: 0 8px 22px rgba(0, 0, 0, 0.5);
  border: 1px solid transparent;
}
.palette-ghost.kind-meal { background: var(--purple); }
.palette-ghost.kind-meeting, .palette-ghost.kind-setup { background: var(--orange); }
.palette-ghost.kind-editing { background: var(--good); }
.palette-ghost.kind-dump { background: var(--teal); }
.palette-ghost.kind-blockout {
  background-color: #050505;
  background-image: repeating-linear-gradient(135deg, rgba(255,255,255,0.1) 0 2px, transparent 2px 10px);
  border-color: #55585f; color: #ddd;
}
.sheet-link { font-size: 11px; color: var(--dim); text-decoration: none; border-bottom: 1px dotted var(--panel-border); }
.sheet-link:hover { color: var(--text); }

@media (max-width: 900px) {
  .assign-layout { grid-template-columns: 1fr; }
  .assign-layout.bleed-layout { width: auto; margin-left: 0; margin-right: 0; padding: 0; }
  .assign-side { order: -1; position: static; }   /* on a phone it leads, no need to pin */
}

/* ---- Conflicts ---- */
.issue {
  display: flex; gap: 10px; align-items: flex-start; padding: 10px 12px; border-radius: 10px;
  border: 1px solid var(--panel-border); background: var(--panel); margin-bottom: 8px;
}
.issue .ico { flex-shrink: 0; font-size: 15px; line-height: 1.3; }
.issue .msg { flex: 1; min-width: 0; font-size: 13px; }
.issue .msg b { font-weight: 600; }
.issue .sub { color: var(--dim); font-size: 12px; margin-top: 2px; }
.issue.high { border-color: var(--bad); background: #241417; }
.issue.med { border-color: var(--warn); }
.issue.low { border-color: var(--panel-border); }

/* ---- Crew grid ---- */
.crew-row { display: flex; gap: 10px; align-items: center; }

/* Three standing teams, toggled per person. Filled means they're on it. */
/* which team someone is on, wherever their name appears */
.team-mark { display: inline-flex; gap: 3px; margin-left: 6px; vertical-align: middle; }
.team-mark-dot { width: 8px; height: 8px; border-radius: 50%; display: inline-block; }

.team-dots { display: flex; gap: 6px; margin-right: 4px; }
.team-dot {
  width: 20px; height: 20px; border-radius: 50%; padding: 0; cursor: pointer;
  background: transparent; border: 2px solid var(--dot); opacity: 0.45;
}
.team-dot:hover { opacity: 0.8; }
.team-dot.on { background: var(--dot); opacity: 1; }
.crew-avatar {
  width: 30px; height: 30px; border-radius: 50%; flex-shrink: 0;
  display: flex; align-items: center; justify-content: center;
  font-size: 12px; font-weight: 700; color: #0c0e12;
}
/* When stages carry the colour, people are shown on neutral slate instead. */
.crew-avatar.neutral { color: var(--text); border: 1px solid #464e5e; }
.crew-name { font-weight: 600; }
.swatch-row { display: flex; gap: 8px; flex-wrap: wrap; margin-top: 6px; }
.swatch {
  width: 32px; height: 32px; border-radius: 8px; border: 2px solid transparent; cursor: pointer;
  display: flex; align-items: center; justify-content: center; color: #0c0e12; font-weight: 700; font-size: 15px; padding: 0;
}
.swatch.on { border-color: var(--text); }
/* A real colour picker, styled to sit in the same row as the fixed swatches
   rather than looking like a stray native form control. */
input[type="color"].swatch {
  /* .modal input, .modal select, .modal textarea sets width:100% with higher
     specificity than plain .swatch's width:32px — this is the one place
     that picker actually lives, so it needs its own explicit size to win. */
  width: 32px; height: 32px; flex-shrink: 0;
  border: 2px dashed var(--panel-border); background: var(--panel-2) conic-gradient(red, yellow, lime, aqua, blue, magenta, red);
  padding: 3px;
}
input[type="color"].swatch::-webkit-color-swatch-wrapper { padding: 0; }
input[type="color"].swatch::-webkit-color-swatch { border: none; border-radius: 5px; }
input[type="color"].swatch:hover { border-color: var(--text); }
/* A tiny live demo of what each toggle actually does, sitting right in its
   own chip — so "Diagonal lines" reads as an actual diagonal pattern, not
   just a phrase you have to turn on to find out. */
.chip-swatch {
  display: inline-block; width: 14px; height: 14px; border-radius: 4px;
  margin-right: 6px; vertical-align: -3px; background: var(--sw, var(--dim));
}
.chip-swatch-hatch { background-image: repeating-linear-gradient(135deg, rgba(255,255,255,0.4) 0 2px, transparent 2px 5px); }
.chip-swatch-border { border: 2px solid #fff; }
.chip-swatch-transparent { opacity: 0.32; }
.chip-swatch-flash { animation: kind-flash-glow 1.3s ease-in-out infinite; }
@media (prefers-reduced-motion: reduce) { .chip-swatch-flash { animation: none; box-shadow: 0 0 0 2px var(--warn-strong) inset; } }
.crew-role { font-size: 12px; color: var(--dim); }

.load-bar { height: 8px; background: #2a2e38; border-radius: 4px; overflow: hidden; margin-top: 7px; max-width: 560px; }
/* The bar is a measurement, not an identity — so it stays neutral until
   someone is meaningfully above or below the average. */
.load-fill { height: 100%; border-radius: 4px; background: #4a5568; }
.load-fill.bad { background: var(--bad); }
.load-fill.warn { background: var(--warn); }
.load-fill.good { background: var(--good); }
.load-fill.dim { background: #2f3440; }

/* ---- Event code screen ---- */
.code-screen { max-width: 380px; margin: 14vh auto 0; text-align: center; }
/* one line, and never a lonely word on a second row if it ever does wrap */
.code-screen .subtitle { text-wrap: balance; }
.code-screen h1 { font-size: 22px; margin-bottom: 6px; }
.code-screen input {
  width: 100%; text-align: center; font-size: 22px; font-weight: 700;
  letter-spacing: 0.18em; text-transform: uppercase; padding: 14px;
}
.code-screen .actions { display: flex; margin-top: 12px; }
.code-screen .actions button { flex: 1; }
.recent { margin-top: 30px; padding-top: 18px; border-top: 1px solid var(--panel-border); }
.recent-label {
  font-size: 11px; text-transform: uppercase; letter-spacing: 0.06em;
  color: var(--dim); margin-bottom: 8px;
}
.recent-item {
  display: block; padding: 11px 12px; margin-bottom: 6px; border-radius: 8px;
  border: 1px solid var(--panel-border); color: var(--text);
  text-decoration: none; font-weight: 600;
}
.recent-item:hover { border-color: var(--accent); }

/* ---- Individual call sheet ---- */
/* Everything down to the call time itself stays put while the day's sets
   scroll underneath — the name, the day picker, the call/next-up hero are
   all things worth having on screen the whole time, not just at the top. */
.sheet-sticky { position: sticky; top: 0; z-index: 5; background: var(--bg); margin: 0 -16px; padding: 0 16px; }

/* The hero: the call time itself, big and obvious, until that time actually
   arrives — then it switches to what's on now (or a countdown to what's
   next), since the call time stops being the useful thing to look at. */
.call-time {
  font-size: 13px; text-transform: uppercase; letter-spacing: 0.06em; font-weight: 700; color: var(--dim);
  margin-bottom: 20px; padding-bottom: 16px; border-bottom: 1px solid var(--panel-border);
  transition: margin 0.15s ease, padding 0.15s ease;
}
.call-time b {
  display: block; color: var(--text); font-size: 44px; font-weight: 800;
  letter-spacing: -0.01em; margin-top: 4px; font-variant-numeric: tabular-nums;
  transition: font-size 0.15s ease;
}
/* An artist name isn't a number — no need for it to carry the same huge
   weight as the call time itself. */
.call-time b.next-item { font-size: 26px; font-variant-numeric: normal; }
.call-time .countdown {
  display: block; font-size: 20px; font-weight: 700; color: var(--accent);
  margin-top: 2px; font-variant-numeric: tabular-nums; text-transform: none; letter-spacing: normal;
  transition: font-size 0.15s ease;
}

/* Once you've actually scrolled down into the day, the hero is just eating
   space above the fold — shrink it down to a compact strip so more of the
   list is visible, same idea as an app's nav bar condensing on scroll. */
.sheet-sticky.compact .title-block .subtitle { display: none; }
.sheet-sticky.compact .title-block h1 { font-size: 15px; }
.sheet-sticky.compact .broadcast-banner { display: none; }
.sheet-sticky.compact .call-time { margin-bottom: 8px; padding-bottom: 8px; font-size: 11px; }
.sheet-sticky.compact .call-time b { font-size: 20px; }
.sheet-sticky.compact .call-time b.next-item { font-size: 15px; }
.sheet-sticky.compact .call-time .countdown { font-size: 13px; margin-top: 0; }

/* what's already happened is out of your hands — say so quietly */
.set-card.gone { opacity: 0.35; filter: grayscale(1); }
.set-card.is-live { border-color: var(--accent); background: #14202f; }
/* on the lineup but not ours — present for context, quiet about it */
.set-card.not-filming { opacity: 0.55; }
.set-card.not-filming .artist { font-weight: 500; }

.now-line {
  position: relative; height: 1px; background: var(--bad);
  margin: 18px 0 14px;
}
.now-line span {
  position: absolute; left: 0; top: -9px;
  background: var(--bad); color: #fff; font-size: 10px; font-weight: 800;
  letter-spacing: 0.08em; text-transform: uppercase; padding: 1px 6px; border-radius: 4px;
}

.info-card { padding: 12px 14px; }
.info-title { font-weight: 600; font-size: 14px; }
.info-text { color: var(--dim); font-size: 13px; margin-top: 3px; white-space: pre-wrap; line-height: 1.5; }
.info-link { display: inline-block; margin-top: 8px; font-size: 13px; font-weight: 600; }

/* ---- Event dashboard ---- */
.event-card { display: flex; align-items: center; gap: 12px; flex-wrap: wrap; }
.event-main { flex: 1; min-width: 180px; }
.event-name { font-size: 17px; font-weight: 600; color: var(--text); text-decoration: none; display: block; }
.event-name:hover { color: var(--accent); }
.event-card .meta { font-size: 12px; color: var(--dim); margin-top: 2px; }
.event-card .dim-line { color: #5c6472; }
.event-actions { display: flex; gap: 6px; align-items: center; flex-wrap: wrap; }

/* ---- Modal ---- */
.modal-back {
  position: fixed; inset: 0; background: rgba(6,8,12,0.72); z-index: 40;
  display: flex; align-items: flex-start; justify-content: center; padding: 24px 12px; overflow-y: auto;
  backdrop-filter: blur(3px);
}
.modal {
  background: var(--panel); border: 1px solid var(--panel-border); border-radius: 14px;
  width: 100%; max-width: 520px; padding: 18px; margin: auto 0;
}
.modal h3 { margin: 0 0 12px; font-size: 16px; }
.modal .grid2 { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; }
.modal .grid3 { display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 10px; }
.modal .field-block { margin-bottom: 10px; }
.modal input, .modal select, .modal textarea { width: 100%; }
.modal .actions { display: flex; gap: 8px; margin-top: 16px; }
/* On a full page a pick list should use the page. Only inside a dialog does it
   need capping, so the dialog's own buttons stay reachable. */
.pick-list { display: flex; flex-direction: column; gap: 6px; }
.modal .pick-list { max-height: 46vh; overflow-y: auto; }
.pick {
  display: flex; align-items: center; gap: 10px; padding: 9px 10px; border-radius: 8px;
  border: 1px solid var(--panel-border); cursor: pointer; background: var(--panel-2);
}
.pick:hover { border-color: var(--accent); }
.pick.on { border-color: var(--good); background: #13251d; }
.pick .who { flex: 1; min-width: 0; }
.pick .busy { font-size: 11px; color: var(--bad); font-weight: 600; }
.pick .free { font-size: 11px; color: var(--dim); }

/* ---- Live view ---- */
.now-grid { display: grid; gap: 10px; grid-template-columns: repeat(auto-fit, minmax(260px, 1fr)); }
.now-card {
  background: var(--panel); border: 1px solid var(--panel-border); border-radius: 12px; padding: 14px;
}
.now-card.live { border-color: var(--accent); background: #14202f; }
.now-card.warn { border-color: var(--warn); }
.now-card.over { border-color: var(--bad); background: #241417; }
.now-card .stage-name { font-size: 11px; text-transform: uppercase; letter-spacing: 0.05em; color: var(--dim); }
.now-card .artist { font-size: 20px; font-weight: 700; margin-top: 3px; }
.now-card .countdown { font-size: 22px; font-weight: 700; font-variant-numeric: tabular-nums; margin-top: 8px; }
.now-card .countdown.warn { color: var(--warn); }
.now-card .countdown.over { color: var(--bad); }
.now-card .next { font-size: 12px; color: var(--dim); margin-top: 10px; padding-top: 10px; border-top: 1px solid var(--panel-border); }
.progress-track { margin-top: 10px; height: 6px; background: #2a2e38; border-radius: 4px; overflow: hidden; }
.progress-fill { height: 100%; background: var(--accent); transition: width 0.4s linear; }
.progress-fill.warn { background: var(--warn); }
.progress-fill.over { background: var(--bad); width: 100% !important; }

.badge-live {
  display: inline-block; font-size: 10px; font-weight: 700; letter-spacing: 0.04em;
  padding: 3px 8px; border-radius: 4px; background: #163a2e; color: #7fe3b4;
}

.empty { text-align: center; padding: 50px 20px; color: var(--dim); }

.fab {
  position: fixed; bottom: 18px; right: 18px; z-index: 20;
  background: var(--accent); border-color: var(--accent); color: #fff; font-weight: 600;
  border-radius: 999px; padding: 13px 20px; box-shadow: 0 4px 18px rgba(0,0,0,0.45);
}

/* Stands in for a confirm dialog on anything that's just one Ctrl+Z away
   from undone — says what happened without stopping you to ask first. */
.toast {
  position: fixed; left: 50%; bottom: 22px; transform: translateX(-50%); z-index: 60;
  display: flex; align-items: center; gap: 10px;
  background: var(--panel-2); border: 1px solid var(--panel-border); border-radius: 10px;
  padding: 10px 12px 10px 16px; box-shadow: 0 8px 24px rgba(0,0,0,0.5);
  font-size: 13px; color: var(--text);
  animation: toast-in 0.2s ease-out;
}
@keyframes toast-in { from { opacity: 0; transform: translate(-50%, 8px); } to { opacity: 1; transform: translate(-50%, 0); } }

.help { font-size: 12px; color: var(--dim); line-height: 1.55; }
.help code { background: var(--panel); padding: 1px 5px; border-radius: 4px; font-size: 11px; }

.link-list { display: flex; flex-direction: column; gap: 8px; }
.share-label { font-size: 13px; font-weight: 700; margin: 6px 0 -2px; }
.share-label:first-child { margin-top: 0; }
.link-list a { font-size: 13px; text-decoration: none; }
.copy-field { display: flex; gap: 6px; align-items: center; }
.copy-field input { flex: 1; font-size: 12px; color: var(--dim); }

@media (max-width: 640px) {
  /* Actions drop onto their own full-width row so the artist name keeps the
     space it needs — otherwise the status select squeezes it to two letters. */
  .set-card { flex-wrap: wrap; }
  .set-card .when { width: 74px; }
  .set-card .body { flex: 1 1 60%; }
  .set-card .actions { flex: 1 0 100%; justify-content: flex-end; margin-top: 8px; }
  .set-card .status-select { flex: 1; }
  .modal .grid2, .modal .grid3 { grid-template-columns: 1fr; }
}

/* ---- Broadcast notice ---- */
.broadcast-banner {
  display: flex; align-items: flex-start; gap: 10px; margin-bottom: 14px;
  padding: 11px 13px; border-radius: 10px;
  border: 1px solid var(--warn); background: #2a2312;
}
.broadcast-icon { font-size: 16px; line-height: 1.2; }
.broadcast-body { flex: 1; min-width: 0; }
.broadcast-text { font-weight: 600; font-size: 14px; }
.broadcast-meta { color: var(--dim); font-size: 11px; margin-top: 2px; }

/* ---- "Your calls changed" ----
   Sibling of the broadcast notice above, and deliberately not the same colour.
   A broadcast is the production office talking to the whole crew; this is one
   person's own day having moved under them while the page sat in their pocket.
   So: accent blue on the panel tint the active day tab already uses, rather
   than the amber that means "read this, everyone". */
.calls-changed {
  display: flex; flex-wrap: wrap; align-items: center; gap: 8px;
  margin-bottom: 12px; padding: 9px 11px; border-radius: 10px;
  border: 1px solid var(--accent); background: #16233a;
}
/* Collapsed, the whole head is the tap target — a count read at arm's length
   in the dark, not a chevron to hit. */
.calls-changed-head {
  flex: 1; min-width: 0; display: flex; align-items: baseline; gap: 6px;
  background: none; border: none; padding: 0; color: var(--text);
  font: inherit; text-align: left; cursor: pointer;
}
.calls-changed-title { font-weight: 600; font-size: 14px; }
.calls-changed-count { color: var(--dim); font-size: 12px; }
.calls-changed-chev { color: var(--dim); font-size: 11px; margin-left: auto; padding-left: 6px; }
.calls-changed-dismiss {
  flex: 0 0 auto; background: none; border: none; padding: 2px 0;
  color: var(--dim); font: inherit; font-size: 12px; cursor: pointer;
  text-decoration: underline; text-underline-offset: 2px;
}
.calls-changed-dismiss:hover { color: var(--text); }
.calls-changed-list { flex: 1 0 100%; margin-top: 2px; }
.calls-changed-day {
  font-size: 11px; text-transform: uppercase; letter-spacing: 0.04em;
  color: var(--dim); margin: 6px 0 2px;
}
/* A coloured rule per kind, so "what arrived" and "what went" separate before
   a word of it is read. Same palette meanings as everywhere else on the app:
   green added, red gone, blue moved, amber the call time itself. */
.calls-changed-line {
  font-size: 13px; line-height: 1.45; padding: 3px 0 3px 9px;
  border-left: 2px solid var(--panel-border);
}
.calls-changed-line.added { border-left-color: var(--good); }
.calls-changed-line.removed { border-left-color: var(--bad); color: var(--dim); }
.calls-changed-line.moved, .calls-changed-line.stage { border-left-color: var(--accent); }
.calls-changed-line.call { border-left-color: var(--warn); font-weight: 600; }

/* Scrolled down, the detail goes but the count and its Dismiss stay. The
   broadcast banner above disappears outright at this point because it has
   already been read; this one has not been acknowledged yet, and hiding the
   only control that acknowledges it would strand it there. */
.sheet-sticky.compact .calls-changed-list { display: none; }

/* ---- Card logging ---- */
.card-log { display: flex; align-items: center; gap: 8px; margin-top: 8px; }
.card-log span { font-size: 11px; text-transform: uppercase; letter-spacing: 0.04em; color: var(--dim); }
.card-log input { flex: 1; max-width: 260px; font-size: 13px; padding: 6px 9px; font-variant-numeric: tabular-nums; }
/* One rule per row: what it is, why it matters, and its control on the right. */
.rule-row {
  display: flex; align-items: center; gap: 16px;
  padding: 12px 0; border-bottom: 1px solid var(--panel-border);
}
.rule-row:last-of-type { border-bottom: none; }
.rule-text { flex: 1; min-width: 0; }
.rule-name { font-size: 14px; font-weight: 600; }
.rule-why { font-size: 12px; color: var(--dim); margin-top: 2px; line-height: 1.45; }
.rule-control { display: flex; align-items: center; gap: 6px; flex-shrink: 0; }
.rule-control input[type="number"] { width: 72px; text-align: center; }
.rule-control span { font-size: 12px; color: var(--dim); }
@media (max-width: 560px) {
  .rule-row { flex-direction: column; align-items: flex-start; gap: 8px; }
}

.toggle-row { display: flex; align-items: flex-start; gap: 10px; font-size: 13px; color: var(--dim); cursor: pointer; }
.toggle-row input { width: 18px; height: 18px; accent-color: var(--accent); flex-shrink: 0; margin-top: 1px; }
.toggle-row b { color: var(--text); }
.print-only { display: none; }

/* ---- Wall: full-screen vertical timeline ----
   Positions are percentages of a time window anchored to the clock, so the
   whole board creeps upward on its own and never needs scrolling. */
.wv { flex: 1; min-height: 0; display: flex; flex-direction: column; }
.wv-days { flex: 0 0 auto; margin-bottom: calc(0.6vh * var(--wall-scale)); }
.wv-days .day-tab { font-size: clamp(12px, calc(0.95vw * var(--wall-scale)), 20px); padding: 0.5em 1em; }
.wv-head { position: relative; display: flex; gap: calc(0.6vw * var(--wall-scale)); flex: 0 0 auto; }
.wv-col-head {
  flex: 1; min-width: 0; padding: calc(0.7vh * var(--wall-scale)) calc(0.8vw * var(--wall-scale));
  font-size: clamp(12px, calc(1.15vw * var(--wall-scale)), 28px); font-weight: 800;
  text-transform: uppercase; letter-spacing: 0.05em;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.wv-scroll {
  flex: 1; min-height: 0; overflow-y: auto; overscroll-behavior: contain;
  -webkit-overflow-scrolling: touch; touch-action: pan-y; overflow-anchor: none;
}
.wv-scroll::-webkit-scrollbar { width: 8px; }
.wv-scroll::-webkit-scrollbar-thumb { background: #2a2e38; border-radius: 4px; }
.wv-body { position: relative; display: flex; gap: calc(0.6vw * var(--wall-scale)); touch-action: pan-y; }

/* Only visible once someone has touched the board. */
.wv-return {
  position: fixed; bottom: 3vh; left: 50%; transform: translateX(-50%);
  background: var(--accent); color: #fff; font-weight: 700;
  font-size: clamp(12px, calc(1vw * var(--wall-scale)), 22px);
  padding: 0.6em 1.4em; border-radius: 999px; cursor: pointer; z-index: 20;
  box-shadow: 0 4px 18px rgba(0,0,0,0.5);
  opacity: 0; pointer-events: none; transition: opacity 0.3s;
}
.wv-return.show { opacity: 1; pointer-events: auto; }
.wv-gutter { flex: 0 0 calc(5vw * var(--wall-scale)); min-width: 54px; position: relative; touch-action: pan-y; }
.wv-head .wv-gutter { flex: 0 0 calc(5vw * var(--wall-scale)); min-width: 54px; }
.wv-tick { position: absolute; left: 0; right: 0; border-top: 1px solid #23262f; }
.wv-tick.half { border-top-style: dotted; opacity: 0.5; }
.wv-tick span {
  position: absolute; right: calc(0.6vw * var(--wall-scale)); top: 50%; transform: translateY(-50%);
  font-size: clamp(11px, calc(0.95vw * var(--wall-scale)), 22px);
  color: var(--dim); font-variant-numeric: tabular-nums; white-space: nowrap;
}
.wv-col {
  flex: 1; min-width: 0; position: relative;
  background: #101319; border-radius: 10px; touch-action: pan-y;
}
/* touch-action on the scrolling container above is enough in Chrome, but
   Safari on iPad doesn't reliably carry that restriction down to a touch
   landing on a deeply nested, absolutely-positioned child — a pinch would
   start a real native page-zoom instead of reaching this code at all,
   which is exactly "works off to the side, not on top of a block". Setting
   it again directly on the blocks themselves is what actually closes that
   gap (same fix already in place for the crew/assign grid's own blocks). */
.wv-block {
  position: absolute; left: 3%; right: 3%; overflow: hidden; touch-action: pan-y;
  border-radius: 10px; padding: calc(0.6vh * var(--wall-scale)) calc(0.7vw * var(--wall-scale));
  background: var(--panel); border: 1px solid var(--panel-border); border-left: 4px solid var(--warn);
  display: flex; flex-direction: column; justify-content: center;
}
/* Red is reserved for "on air right now" — covered or not. Everything else
   we're filming sits at a quieter yellow-orange until it actually goes live. */
.wv-block.live {
  background: #241417; border-color: var(--bad); border-left-color: var(--bad);
  box-shadow: 0 0 0 2px rgba(255,92,92,0.35);
}
.wv-block.done { opacity: 0.4; }
.wv-block.unassigned {
  background: #16181d; border-color: #23262f; border-left-color: #3a3f4b;
}
.wv-block.unassigned .wv-crew { color: #8a90a0; }
/* Live AND nobody's on it — still red (it's live, that's the point), but the
   crew line shouts too, since that's the actual problem here. */
.wv-block.uncovered-live {
  background: #241417; border-color: var(--bad); border-left-color: var(--bad);
  box-shadow: 0 0 0 2px rgba(255,92,92,0.35);
}
.wv-block.uncovered-live .wv-crew { color: var(--bad); font-weight: 700; }
.wv-artist {
  font-size: clamp(13px, calc(1.5vw * var(--wall-scale)), 38px); font-weight: 800;
  line-height: 1.1; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.wv-crew {
  font-size: clamp(11px, calc(1vw * var(--wall-scale)), 24px); color: var(--dim);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis; margin-top: 2px;
}
.wv-times {
  font-size: clamp(10px, calc(0.85vw * var(--wall-scale)), 20px); color: var(--dim);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis; margin-top: 2px;
  font-variant-numeric: tabular-nums;
}
.wv-cap .wv-times { color: rgba(255, 255, 255, 0.75); }
.wv-block.not-filming { background: #14161b; border-color: #23262f; border-left-color: #2a2e38; opacity: 0.6; }
/* The "filming view" set: a quiet diagonal hatch for the whole thing —
   happening, not necessarily on camera — with the capture window cut out of
   it as a solid, bold team colour, same language as the Assign page's
   hatch + clear-window pairing, just louder for reading across a room. */
.wv-cap-wrap {
  position: absolute; left: 3%; right: 3%; overflow: hidden; touch-action: pan-y;
  border-radius: 10px; border: 1px solid var(--panel-border);
  background-color: #101319;
  background-image: repeating-linear-gradient(135deg, rgba(255, 255, 255, 0.06) 0 2px, transparent 2px 10px);
}
.wv-cap-wrap.done { opacity: 0.4; }
.wv-cap-wrap-label {
  position: absolute; left: calc(0.7vw * var(--wall-scale)); top: calc(0.6vh * var(--wall-scale)); right: calc(0.7vw * var(--wall-scale));
  font-size: clamp(13px, calc(1.5vw * var(--wall-scale)), 38px); font-weight: 800; color: var(--dim);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.wv-cap {
  position: absolute; left: 2%; right: 2%; overflow: hidden; touch-action: pan-y;
  border-radius: 8px; padding: calc(0.6vh * var(--wall-scale)) calc(0.7vw * var(--wall-scale));
  border: 2px solid transparent;
  display: flex; flex-direction: column; justify-content: center;
  color: #fff;
}
.wv-cap.live { box-shadow: 0 0 0 2px rgba(255, 255, 255, 0.55); }
.wv-cap.done { opacity: 0.6; }
.wv-cap .wv-artist { color: #fff; }
.wv-cap .wv-crew { color: rgba(255, 255, 255, 0.85); }
.wv-restrict {
  font-size: clamp(10px, calc(0.85vw * var(--wall-scale)), 20px); color: #ffbe7a;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis; margin-top: 2px;
}
.wv-left {
  font-size: clamp(11px, calc(1vw * var(--wall-scale)), 24px); font-weight: 800;
  font-variant-numeric: tabular-nums; margin-top: 3px;
}
.wv-now { position: absolute; left: 0; right: 0; height: 2px; background: var(--bad); z-index: 5; }
.wv-now span {
  position: absolute; left: 0; top: 50%; transform: translateY(-50%);
  background: var(--bad); color: #fff; font-size: clamp(9px, calc(0.7vw * var(--wall-scale)), 17px);
  font-weight: 800; text-transform: uppercase; letter-spacing: 0.08em;
  padding: 0.15em 0.5em; border-radius: 4px;
}
/* Every day is in the DOM at once — this is what stands in for "loading":
   the day you're not currently reading (see updateDayFocus, which drives
   --day-fade per section based on distance from the focus line) sits
   desaturated and a little dimmed, and resolves to full colour as its
   content approaches where you're actually looking. */
.rs-timeline {
  position: relative;
  filter: grayscale(var(--day-fade, 0));
  opacity: calc(1 - var(--day-fade, 0) * 0.55);
  transition: filter 0.08s linear, opacity 0.08s linear;
}
/* The stage names float once, for the whole timeline — not per day. It
   lives inside .sheet-sticky (already position:sticky) rather than being
   sticky itself, so there's exactly one of these ever on screen instead of
   a new one popping in and colliding with the last few pixels of the
   previous day's fade every time a boundary scrolls past. */
.rs-wv-head-global { padding-bottom: 4px; }
/* The real dead stretch at the end of a day — nothing scheduled, so the
   grid fades out toward the middle of it (dark in the middle, clear at
   both ends) instead of just trailing off abruptly, with the next day's
   name floating back into view low in the fade, right where its own
   content is about to resume. Not a control — every day's already on the
   page, this is just marking what's coming. */
.rs-day-gap {
  position: absolute; left: 0; right: 0; z-index: 4; pointer-events: none;
  background: linear-gradient(to bottom, transparent 0%, var(--bg) 38%, var(--bg) 72%, transparent 100%);
}
/* The day you're about to reach — the main read, so it's plain bright
   text (no pill) sitting a bit above the very bottom of the gap, well
   before its own content starts. */
.rs-day-gap-label {
  position: absolute; left: 0; right: 0; bottom: 42px; text-align: center;
  font-size: 16px; font-weight: 700; color: var(--text); letter-spacing: 0.02em;
}
/* The day you just left — secondary to the label above, purely there so
   scrolling back up through the gap still tells you what you're passing
   through instead of just repeating the same "next day" name. */
.rs-day-gap-end {
  position: absolute; left: 0; right: 0; top: 10px; text-align: center;
  font-size: 12px; font-weight: 700; color: var(--dim); letter-spacing: 0.02em;
}

/* Setup's "Import backup JSON" control, which is a <label> around a hidden file
   input rather than a button — see the note beside the shared button rules near
   the top of this file. It sits here for no better reason than that the plot
   tab once had an upload of its own. */
.upload-btn { display: inline-flex; align-items: center; cursor: pointer; }

/* ---- The planner's Plot tab ----
   plot.html itself in a frame, parked over the tab body by syncPlotFrame().
   Fixed, because it is deliberately not a child of the view it covers —
   render() rewrites that markup constantly and a rewritten iframe reloads. The
   slot is a zero-height anchor left in the flow purely to say where the frame's
   rectangle is.

   The tab used to draw its own read-only picture of the plot instead, in a
   `.plot-wrap`; those rules went with it. The wall display still draws one and
   has its own `.w-plot-wrap` further down — a different rule, which is what
   made this pair look live when it was not. */
.plot-frame {
  position: fixed; z-index: 1; border: 0; display: block;
  background: var(--bg);
  transition: left .28s ease, top .28s ease, width .28s ease, height .28s ease, border-radius .28s ease;
}
.plot-frame[hidden] { display: none; }
.plot-frame-slot { height: 0; }
.plot-focus-mode { overflow: hidden; }
.plot-focus-animating::after {
  content: ""; position: fixed; inset: 0; z-index: 20; pointer-events: none;
  background: #000; animation: plot-focus-shade .32s ease both;
}
@keyframes plot-focus-shade { 0%, 100% { opacity: 0; } 45% { opacity: .34; } }
.plot-focus-mode #app > .top-bar,
.plot-focus-mode #app > .tabs,
.plot-focus-mode #app > .broadcast-banner,
.plot-focus-mode #app > .issue { display: none; }
/* 360, not 356, and the 4px is arithmetic rather than taste. This pill is in
   the PARENT document; the row it has to line up with is inside the iframe,
   which is inset 16px from the parent's right edge, and that row sits
   356px (the 320px tool column, plus 24 to clear it, plus 12 of margin INSIDE
   the map card) in from the iframe's own right. So 356 + 16 = 372 is where
   the parent has to put this for the two right edges to be the same edge. */
.plot-focus-actions {
  position: fixed; z-index: 8; right: calc(372px + env(safe-area-inset-right, 0px));
  bottom: calc(18px + env(safe-area-inset-bottom, 0px)); display: flex; flex-direction: column; gap: 7px;
}
.plot-focus-toggle {
  padding: 9px 13px; border: 1px solid var(--panel-border); border-radius: 10px;
  background: rgba(20,23,29,.92); color: var(--text); box-shadow: 0 6px 20px rgba(0,0,0,.35);
}
.plot-focus-mode .plot-focus-actions { right: calc(14px + env(safe-area-inset-right, 0px)); }
.plot-focus-mode.plot-focus-tools-open .plot-focus-actions { right: calc(356px + env(safe-area-inset-right, 0px)); }
@media (max-width: 899px) {
  .plot-focus-actions,
  .plot-focus-mode.plot-focus-tools-open .plot-focus-actions { right: calc(14px + env(safe-area-inset-right, 0px)); }
}
body.plot-parent-focus:not(.plot-focus-tools-open) .plot-top,
body.plot-parent-focus:not(.plot-focus-tools-open) .plot-sheet,
body.plot-parent-focus:not(.plot-focus-tools-open) .plot-wires,
body.plot-parent-focus:not(.plot-focus-tools-open) .plot-layers,
body.plot-parent-focus:not(.plot-focus-tools-open) .plot-map-controls,
body.plot-parent-focus:not(.plot-focus-tools-open) .plot-cameras { display: none !important; }
body.plot-parent-focus:not(.plot-focus-tools-open) .plot-tools,
body.plot-parent-focus:not(.plot-focus-tools-open) .plot-palette { display: none !important; }
body.plot-parent-focus:not(.plot-focus-tools-open) .plot-stage { inset: 0 !important; border-radius: 0 !important; }
body.plot-parent-focus .plot-legend-slot { bottom: 12px !important; }
/* Same "break out of .app.wide's capped column" trick Assign/Crew use
   (.assign-layout.bleed-layout above), so the Plot tab reaches the same
   edges they do on a wide monitor instead of sitting capped at 1440px while
   they go edge-to-edge. Can't reuse that rule as-is: it's a compound
   selector tied to .assign-layout's own grid, and — unlike that rule, which
   leaves its box at a full 100vw and insets its grid children with padding
   — this element has no children for padding to inset. syncPlotFrame()
   copies this box's own rect straight onto the iframe, so the 16px gutter
   has to be baked into the box's width/margin instead, or the frame would
   land flush against the actual browser edges with no gutter at all. */
.plot-frame-slot.bleed-layout {
  width: calc(100vw - 32px);
  margin-left: calc(50% - 50vw + 16px);
  margin-right: calc(50% - 50vw + 16px);
}
@media (max-width: 900px) {
  /* Same breakpoint as .assign-layout.bleed-layout and the plot's own
     docked tool column — below it, sit back inside .app.wide's normal
     padding like any other element, same as before this rule existed. */
  .plot-frame-slot.bleed-layout { width: auto; margin-left: 0; margin-right: 0; }
}

/* ---- Gear tab ----
   One list read top to bottom, headings dividing it by camera, with the
   totals parked beside it. Deliberately NOT a grid of cards — this is a
   packing list, and the first version made it eleven panels to scan around
   instead of one list to go down. Rows are hairline-separated rather than
   boxed for the same reason: the eye should run down the quantity column. */
.gear-layout { display: flex; gap: 20px; align-items: flex-start; }
.gear-list {
  flex: 1; min-width: 0;
  background: var(--panel); border: 1px solid var(--panel-border); border-radius: 12px;
  padding: 4px 14px 14px;
  /* The width of one trailing icon button on an item row. The head row and
     the ghost row have no buttons, so their .gear-gap is derived from this to
     cover the same ground — change it here, not in either place. */
  --gear-btn: 28px;
}
/* EVERY ITEM ROW IS THE SAME ROW. A camera body, a lens, a battery and a
   short cable are all just one line each with a number beside them — the
   list is a tally of things to load, and nothing in it is worth more visual
   weight than anything else. So the section divider is deliberately the
   quietest thing on the page rather than a heading that towers over the
   rows under it, and every row shares one height, one type size and one set
   of column widths, which is what makes the columns line up unbroken from
   the top of the page to the bottom. */
.gear-row, .gear-head-row, .gear-sec-head {
  display: flex; gap: 8px; align-items: center;
  /* One height for every row in the sheet. */
  min-height: 30px;
}
.gear-head-row {
  font-size: 10px; text-transform: uppercase; letter-spacing: .06em; color: var(--dim);
  border-bottom: 1px solid var(--panel-border); padding-bottom: 4px;
  /* The item rows sit inside a section's 1px border, with the section's own
     10px of right padding (the left is cancelled — see .gear-sec .gear-row
     .gear-qty below). The head row sits outside any section, so it takes the
     same insets itself: that is what puts its Qty over the numbers and its
     Note over the notes rather than a border-width to one side of them. */
  padding-left: 1px; padding-right: 11px;
}
.gear-head-row .gear-label { padding-left: 7px; }
/* WHAT BELONGS TO WHAT. Rows and their add button have to read as sitting
   under their own heading, and the first version had this backwards: a rule
   UNDER the heading cut it off from its own rows, and the gap between one
   section and the next was no bigger than the gaps inside one — so an add
   button sat as near the next camera's heading as its own, which is exactly
   how you end up not knowing which one you are about to press.
   So the heading is now a filled band reading as the lid of the block under
   it, its rows are tight against it, and the space between sections is much
   bigger than any space within one. */
.gear-sec {
  border: 1px solid var(--panel-border); border-radius: 10px;
  overflow: hidden; padding-bottom: 2px;
}
.gear-sec + .gear-sec { margin-top: 18px; }
.gear-sec-head {
  background: var(--bg); border-bottom: 1px solid var(--panel-border);
  padding: 6px 10px 6px 0; margin-bottom: 4px;
  font-size: 10px; text-transform: uppercase; letter-spacing: .06em;
  /* The whole band is the button — see the click handler. The grip and the
     copy picker put their own cursors back, being their own controls. */
  cursor: pointer;
}
.gear-sec-head:hover { background: var(--panel-border); }
/* The rows sit inside the section's border now, so they need the padding the
   list used to give them. */
.gear-sec .gear-row { padding-left: 10px; padding-right: 10px; }
/* The quantity is pulled back to the section's own edge, so the numbers run
   in ONE column with the grip in the band above them (which starts at that
   edge, see .gear-grip) and with the Qty heading at the top of the sheet. */
.gear-sec .gear-row .gear-qty { margin-left: -10px; }
/* The grip. Sits in the quantity column so it lines up with the numbers
   running down the sheet rather than pushing every heading out of step with
   the rows under it. */
.gear-grip {
  width: 52px; flex: none; background: none; border: 0; padding: 0;
  color: var(--dim); font-size: 13px; line-height: 1; cursor: grab;
  text-align: center;
  /* Without this a drag on a touch screen scrolls the page instead of
     moving the section — the browser claims the gesture before the first
     pointermove ever arrives. */
  touch-action: none;
}
.gear-grip:hover { color: var(--text); }
.gear-grip:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; border-radius: 4px; }
.gear-grip.is-blank { cursor: default; }
/* Picked up off the page rather than tinted in place: a shadow and a hair of
   scale say "this one is in your hand" far faster than a colour change, and
   they survive being read at a glance mid-drag. z-index keeps it over the
   sections sliding out of its way, which would otherwise paint across it. */
.gear-sec.is-dragging {
  position: relative; z-index: 5;
  border-color: var(--accent);
  box-shadow: 0 10px 26px rgba(0, 0, 0, .45);
  transform-origin: center;
  /* No transition on transform while in hand — it is being positioned every
     pointermove, and easing each one would leave it lagging the finger. The
     settle transition is applied in JS, on release, for that reason. */
  cursor: grabbing;
  /* Promoted for the duration of the gesture only: this is the one element
     being re-positioned every frame, and a layer of its own keeps that off
     the rest of the list's paint. */
  will-change: transform;
}
/* Nothing under the pointer reacts to it while a section is in hand. Without
   this, the rows the dragged section passes over light up their hover
   borders one after another as it goes, which reads as the list flickering
   even though nothing is moving wrongly. Geometry, not hit-testing, is what
   decides where the section drops, so taking pointer events away from the
   list costs the gesture nothing — and the grip holds the pointer capture
   regardless. */
body.gear-dragging .gear-sec { pointer-events: none; }
.gear-sec.is-dragging .gear-grip { cursor: grabbing; }
/* The gap the section leaves behind and the one it is dropping into both
   belong to sections that are sliding, so they get the ease the dragged one
   deliberately does not. */
@media (prefers-reduced-motion: reduce) {
  .gear-sec { transition: none !important; }
}
/* While a section is in hand, nothing else on the page should be selectable
   or hoverable — a drag over text otherwise starts a native selection, the
   same trap the plot's own drags document. */
body.gear-dragging { user-select: none; cursor: grabbing; }
body.gear-dragging input { pointer-events: none; }
.gear-sec-name {
  color: var(--text); font-weight: 700;
  background: none; border: 0; padding: 0; cursor: pointer;
  font: inherit; font-weight: 700; letter-spacing: inherit; text-transform: inherit;
}
.gear-sec-name:hover { color: var(--accent); }
/* Rename and remove, on a made-up section only. Hidden until the bar is
   hovered, like the row delete — a heading is read far more often than it is
   renamed, and two glyphs on every one of them is clutter the rest of the
   time. */
.gear-sec-btn {
  flex: none; width: 22px; height: 20px; padding: 0; border: 0; border-radius: 5px;
  background: none; color: var(--dim); cursor: pointer; font-size: 11px; opacity: 0;
}
.gear-sec-head:hover .gear-sec-btn, .gear-sec-btn:focus-visible { opacity: 1; }
.gear-sec-btn:hover { color: var(--text); background: var(--panel-border); }
@media (hover: none) { .gear-sec-btn { opacity: 1; } }
/* A made-up section is not a camera, and says so quietly rather than by
   looking like a different kind of thing entirely. */
.gear-sec.is-custom .gear-sec-name { color: var(--text); }
.gear-list-actions, .gear-start-actions { display: flex; gap: 8px; flex-wrap: wrap; margin-top: 14px; }
.gear-start-actions { margin-top: 10px; }
.gear-sec-name:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; border-radius: 4px; }

/* WHERE THIS ENDS UP. Pressing a camera, or putting the caret in one of its
   rows, lights the lines it feeds on the totals — and the rows that feed
   them — so the two sides of the tab can be read as one thing.
   Everything not lit is dimmed rather than the lit things being made louder:
   turning one line up leaves twenty others competing with it, where turning
   the twenty down leaves only the answer. */
body.gear-lit .gear-totals [data-total-key],
body.gear-lit .gear-list .gear-row { opacity: .35; transition: opacity .12s ease; }
body.gear-lit .gear-totals [data-total-key].is-lit,
body.gear-lit .gear-list .gear-row.is-lit { opacity: 1; }
.gear-totals [data-total-key].is-lit {
  background: var(--panel-border); border-radius: 6px;
  box-shadow: -6px 0 0 var(--accent); margin-left: 6px; padding-left: 4px;
}
.gear-row.is-lit { background: var(--panel-border); border-radius: 6px; }
/* The heading of the camera being inspected stays legible while its own
   section's other rows are dimmed. */
body.gear-lit .gear-sec.is-lit .gear-sec-head { opacity: 1; }
body.gear-lit .gear-sec-head { opacity: .55; }
body.gear-lit .gear-sec.is-lit .gear-sec-name { color: var(--accent); }
.gear-sec-sub, .gear-sec-count { color: var(--dim); text-transform: none; letter-spacing: 0; font-size: 11px; }
/* Pushed to the right-hand end, so every section's count lines up in a
   column of its own however long the names are. */
.gear-sec-count { margin-left: auto; }
/* The three columns, shared by the heading row and every item row, which is
   the whole reason they align. */
.gear-qty { width: 52px; flex: none; text-align: center; }
/* Said again for the quantity INPUT specifically, because the page-wide
   input[type="number"] rule near the top of this file outranks a bare class
   and was quietly making it 76px — which pushed every column after it on
   every item row 24px right of the heading it was meant to sit under. */
.gear-row input.gear-qty { width: 52px; }
/* Basis 0, not auto: the label is the one column that gives way when a row is
   squeezed, and with an auto basis an INPUT starts from its own intrinsic
   width (a good 150px) while the heading's SPAN starts from the word "Item",
   so the two rows shrank From and Note by different amounts and the columns
   drifted apart the moment there was less room than they wanted. */
.gear-label { flex: 1 1 0; min-width: 0; }
.gear-from { flex: 0 1 130px; min-width: 0; }
.gear-note { flex: 0 1 110px; min-width: 0; }
/* Where an item row has its two icon buttons, the head row and the ghost row
   have this instead: two buttons' width plus the gap between them, so the
   Note column ends at the same place on every row. */
.gear-gap { width: calc(var(--gear-btn) * 2 + 8px); flex: none; }
/* Borderless until touched: twenty rows drawn as twenty boxes is a form,
   and this should read as a sheet that happens to be editable. */
/* Every cell shows its own outline, always — not only under the pointer.
   Hairlines between rows said where one line ended, but nothing said where a
   column began, so a section read as a paragraph rather than as a table. The
   border is faint enough to stay a table and not a form. */
.gear-row input {
  padding: 4px 7px; font-size: 13px; background: transparent;
  border-color: color-mix(in srgb, var(--panel-border) 65%, transparent);
  height: 26px;
}
.gear-row input:hover { border-color: var(--panel-border); }
.gear-row input:focus { background: var(--bg); border-color: var(--accent); }
.gear-row input.gear-note { color: var(--dim); }
.gear-row .icon { opacity: 0; flex: none; width: var(--gear-btn); height: 26px; }
.gear-row:hover .icon, .gear-row .icon:focus { opacity: 1; }
/* No hover to reveal them with, so on a phone or tablet both a row's buttons
   were invisible — send-to-plot and delete simply absent, with no way to find
   out they were there. Same fix .gear-sec-btn already carries just above, for
   exactly the same reason. */
@media (hover: none) {
  .gear-row .icon { opacity: 1; }
}
/* The waiting row. Dimmed so it reads as an invitation rather than as an
   item that is already on the list — the quantity especially, which would
   otherwise look like a real 1 of something unnamed. It brightens to an
   ordinary row on focus, which is the moment it is about to become one. */
/* What the plot says this camera needs and the list has not got. An offer,
   so it reads as quieter than the rows it sits under rather than as an
   error about them. */
.gear-suggest {
  display: flex; align-items: center; gap: 8px; flex-wrap: wrap;
  margin: 4px 10px 2px; padding: 5px 8px; border-radius: 7px;
  background: var(--panel-border); font-size: 11px; color: var(--dim);
}
.gear-suggest-text { flex: 1 1 auto; min-width: 0; }
.gear-suggest-add {
  flex: none; padding: 2px 9px; border-radius: 6px; cursor: pointer;
  background: none; border: 1px solid var(--accent); color: var(--accent); font-size: 11px;
}
.gear-suggest-add:hover { background: var(--accent); color: #fff; }
/* The other half of the same row: saying a line already covers this pin. */
.gear-suggest-link { flex: none; font-size: 11px; padding: 2px 4px; max-width: 150px; }
/* Sources already used on this job, offered under whichever From box has the
   caret. Fixed, because the list it belongs to scrolls — see the element's
   own comment in planner.html — and above the sheet layer so a picker opened
   near the bottom of a section is not clipped by it. */
/* A strip in the list, under the row being edited — not a card floating over
   it. See showGearFromPicker: floating covered the next row down, which is
   where you go next, so the press meant for it was swallowed. */
.gear-from-picker {
  display: flex; gap: 4px; flex-wrap: wrap; align-items: center;
  margin: 2px 10px 4px; padding: 5px 7px; border-radius: 8px;
  background: var(--panel-2); border: 1px solid var(--panel-border);
}
.gear-from-picker[hidden] { display: none; }
.gear-from-chip {
  padding: 3px 9px; border-radius: 999px; cursor: pointer; font-size: 11px;
  background: var(--panel-border); border: 1px solid transparent; color: var(--fg);
  white-space: nowrap;
}
.gear-from-chip:hover { border-color: var(--accent); color: var(--accent); }
.gear-from-chip.is-on { background: var(--accent); color: #fff; }

/* On the map, on no list. Its own block at the foot of the list, in the
   warning tint rather than the ordinary section grey — the whole point is
   that it reads as an outstanding job and not as one more heading. */
.gear-unlisted-sec .gear-sec-head { background: color-mix(in srgb, var(--warn) 18%, var(--panel)); }
.gear-unlisted-sec .gear-sec-name.as-text { color: var(--warn); font-weight: 600; }
.gear-unlisted-row .gear-label { color: var(--fg); }
.gear-unlisted-kind { flex: 0 0 110px; color: var(--dim); font-size: 11px; }
.gear-unlisted-file { flex: 0 0 140px; font-size: 11px; padding: 2px 4px; }

/* The totals are pressable too — the same question from the other end. */
.gear-totals [data-total-key] { cursor: pointer; }
.gear-row.is-ghost { opacity: .5; }
.gear-row.is-ghost:hover, .gear-row.is-ghost:focus-within { opacity: 1; }
.gear-row.is-ghost .gear-qty { color: var(--dim); font-size: 13px; }
.gear-row.is-ghost .gear-label::placeholder { color: var(--dim); }
/* The same kit list, read-only, in the plot's own camera panel — the answer
   to "what am I carrying for this one" asked standing in front of the pin.
   Quantity column on the left like the Gear tab's, so the two read as the
   same list seen twice rather than two different lists. */
/* Kit sent from the Gear tab, waiting to be dropped on the map. Top-left,
   clear of the legend (bottom-left), the view controls (bottom-right) and
   the tools column (right) — the one corner of the map nothing else claims. */
.plot-unplaced {
  position: fixed; z-index: 4; left: 12px; top: calc(var(--plot-top-h, 57px) + 10px);
  max-width: 200px; padding: 8px 10px; border-radius: 10px;
  background: rgba(20, 23, 29, .94); border: 1px solid var(--panel-border);
  box-shadow: 0 6px 20px rgba(0, 0, 0, .35);
  display: flex; flex-direction: column; gap: 4px;
}
.plot-unplaced[hidden] { display: none; }
.plot-unplaced-head {
  font-size: 10px; text-transform: uppercase; letter-spacing: .06em; color: var(--dim);
  display: flex; gap: 6px;
}
.plot-unplaced-head span { margin-left: auto; }
.plot-unplaced-item {
  text-align: left; font-size: 12px; padding: 4px 7px; border-radius: 7px;
  background: var(--panel); border: 1px solid var(--panel-border); color: var(--text);
  cursor: pointer; max-width: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.plot-unplaced-item:hover { border-color: var(--accent); }
/* The one armed to drop reads as armed, the same way every other tool's
   button does. */
.plot-unplaced-item.is-on { background: var(--accent); border-color: var(--accent); color: #fff; }
@media (min-width: 900px) {
  /* Below the docked column's own top inset, so it lines up with the map. */
  body.plot-tools-on .plot-unplaced { top: 24px; left: 24px; }
}
.plot-gear { margin-top: 12px; }
.plot-gear-head {
  font-size: 10px; text-transform: uppercase; letter-spacing: .06em; color: var(--dim);
  display: flex; gap: 6px; align-items: baseline;
  padding-bottom: 4px; border-bottom: 1px solid var(--panel-border);
}
.plot-gear-head span { margin-left: auto; }
.plot-gear-list { list-style: none; margin: 0; padding: 0; }
.plot-gear-list li {
  display: flex; gap: 8px; align-items: baseline;
  padding: 4px 0; font-size: 13px; border-bottom: 1px solid var(--panel-border);
}
.plot-gear-list li:last-child { border-bottom: 0; }
.plot-gear-qty { color: var(--accent); font-weight: 700; flex: none; min-width: 28px; }
.plot-gear-name { flex: 1 1 auto; min-width: 0; }
.plot-gear-note { color: var(--dim); font-size: 11px; flex: none; }
.plot-gear-hint { margin-top: 6px; }
/* The same row with the fields live. Borderless until touched, like the Gear
   tab's own rows, so a list of ten reads as a list rather than ten boxes. */
.plot-gear-row { gap: 6px; }
.plot-gear-row input {
  padding: 3px 6px; font-size: 13px; height: 26px;
  background: transparent; border: 1px solid transparent; border-radius: 6px; color: var(--text);
}
.plot-gear-row input:hover { border-color: var(--panel-border); }
.plot-gear-row input:focus { background: var(--bg); border-color: var(--accent); outline: none; }
/* Both quantity cells declare the SAME box, so the two rows line up. A
   type="number" input carries a UA minimum width that quietly beat a bare
   `width` — it measured 76px against the ghost's 28 — so the basis is pinned
   and min-width released on both. */
.plot-gear-qty-input { flex: 0 0 46px; width: 46px; min-width: 0; text-align: center; }
.plot-gear-label-input { flex: 1 1 auto; min-width: 0; }
.plot-gear-x {
  flex: none; width: 24px; height: 24px; padding: 0; border: 0; border-radius: 6px;
  background: none; color: var(--dim); font-size: 15px; cursor: pointer;
}
.plot-gear-x:hover { color: var(--bad); background: var(--panel-border); }
/* The waiting row at the foot of the panel's kit list. Same treatment as the
   Gear tab's own ghost row — faded until it is pointed at or typed in, so it
   reads as an invitation rather than as a line somebody left blank. */
.plot-gear-row.is-ghost { opacity: .5; }
.plot-gear-row.is-ghost:hover, .plot-gear-row.is-ghost:focus-within { opacity: 1; }
/* The ghost's quantity is text where the real row's is an input, so it has to
   be given the input's box by hand — same basis, same padding, same border —
   or the label beside it starts at a different place. */
.plot-gear-row.is-ghost .plot-gear-qty {
  flex: 0 0 46px; width: 46px; min-width: 0; text-align: center;
  padding: 3px 6px; border: 1px solid transparent; font-size: 13px; line-height: 18px;
  color: var(--dim); font-weight: 400;
}
/* And it ends where the real row's delete button starts, rather than running
   under where that column is. */
.plot-gear-x-gap { flex: none; width: 24px; }

/* The measured length beside a run, and the one line of honesty under the
   list — these are straight-line distances off the map, not cable runs. */
.plot-cable-len { margin-left: auto; flex: none; color: var(--accent); font-weight: 700; font-size: 12px; }
.plot-gear-hint { margin: 6px 0 0; font-size: 11px; color: var(--dim); line-height: 1.4; }
.gear-no-cameras { flex: 1 1 100%; }
.gear-totals { flex: 0 0 250px; position: sticky; top: 12px; }
/* A touch device has no hover, so a delete that only appears on hover is a
   delete that never appears. */
@media (hover: none) {
  .gear-row .icon { opacity: 1; }
  .gear-row input { border-color: var(--panel-border); }
}
.gear-total-group + .gear-total-group { margin-top: 12px; }
.gear-total-from {
  font-size: 10px; text-transform: uppercase; letter-spacing: .06em; color: var(--accent);
  padding-bottom: 3px; border-bottom: 1px solid var(--panel-border); margin-bottom: 2px;
}
.gear-total-list { list-style: none; margin: 0; padding: 0; }
.gear-total-list li {
  display: flex; gap: 8px; padding: 5px 0; font-size: 13px;
  border-bottom: 1px solid var(--panel-border);
}
.gear-total-list li:last-child { border-bottom: 0; }
.gear-total-qty { color: var(--accent); font-weight: 700; flex: none; min-width: 34px; }
/* "6/14" — this camera's share of a total several of them feed. Wider than
   the plain count it replaces, and only while something is selected, so the
   column does not sit permanently sized for a number that is usually not
   there. Not shown when the share IS the total: "14/14" is a fraction
   pretending to be news. */
.gear-total-qty.is-share { min-width: 46px; color: var(--accent); }
@media (max-width: 900px) {
  /* The totals stop being a sidebar and become the last thing on the page —
     on a phone there is no room beside anything.

     align-items has to come back to stretch with it. On a row, flex-start is
     what keeps the totals from stretching to the list's height; turned
     sideways it becomes "size to your own content in the horizontal", so the
     list sized itself to its widest row — 623px on a 375px phone — and it was
     the PAGE that scrolled sideways, gear tab and top bar and tabs and all. */
  .gear-layout { flex-direction: column; align-items: stretch; }
  .gear-totals { flex: 1 1 auto; width: 100%; position: static; }
}

@media (max-width: 640px) {
  /* Five columns of a packing list do not fit across a phone, and this list is
     deliberately five columns — every row the same row, the quantities lining
     up unbroken from the top of the page to the bottom (see the note above
     .gear-row). Stacking each row into two lines would break exactly that, and
     hide the From and Note headings that say which box is which.

     So the list keeps its shape and scrolls sideways INSIDE ITS OWN BOX. The
     min-width is on the rows and section blocks rather than on the list, so
     the scroller is the panel and the page is not — the whole point, since a
     page that scrolls sideways takes the top bar and the tab strip with it. */
  .gear-list { overflow-x: auto; overscroll-behavior-x: contain; }
  .gear-head-row, .gear-sec { min-width: 500px; }

  /* Thumb-sized. The rows are 30px on a desktop because a tally reads better
     tight, but 30px is not something you hit reliably standing up in a truck. */
  .gear-row { min-height: 44px; }
  .gear-row input { min-height: 40px; }
  .gear-row .icon { width: 40px; height: 40px; }
  /* The offer strips stay quiet — 11px text, and they are occasional — but
     their controls still have to be hittable. */
  .gear-suggest-add, .gear-suggest-link, .gear-unlisted-file { min-height: 34px; }
}

/* ---- Wall display ----
   Sized in vw/clamp so the same page fills a laptop or a 55" panel in the
   production office. Everything scales off --wall-scale, which ?scale= sets. */
:root { --wall-scale: 1; }
.wall-body { overflow: hidden; height: 100vh; cursor: default; }
.wall {
  height: 100vh; padding: calc(2.4vh * var(--wall-scale)) calc(3vw * var(--wall-scale));
  display: flex; flex-direction: column; gap: calc(2.2vh * var(--wall-scale));
  font-size: calc(1vw * var(--wall-scale));
}

.w-head { display: flex; justify-content: space-between; align-items: flex-start; gap: 2vw; }
.w-title { font-size: clamp(20px, calc(2.4vw * var(--wall-scale)), 64px); font-weight: 700; line-height: 1.05; }
.w-day { font-size: clamp(12px, calc(1.15vw * var(--wall-scale)), 28px); color: var(--dim); margin-top: 0.3vh; }
.w-clock-block { text-align: right; }
.w-clock {
  font-size: clamp(24px, calc(3.4vw * var(--wall-scale)), 92px); font-weight: 700;
  font-variant-numeric: tabular-nums; line-height: 1;
}
.w-livedot { color: var(--good); font-size: clamp(11px, calc(0.9vw * var(--wall-scale)), 22px); font-weight: 700; letter-spacing: 0.08em; text-transform: uppercase; }
.w-offline { color: var(--bad); font-size: clamp(11px, calc(0.9vw * var(--wall-scale)), 22px); font-weight: 700; }

.w-notice {
  display: flex; gap: 1vw; align-items: center;
  background: #2a2312; border: 2px solid var(--warn); border-radius: 12px;
  padding: calc(1.2vh * var(--wall-scale)) calc(1.2vw * var(--wall-scale));
  font-size: clamp(15px, calc(1.7vw * var(--wall-scale)), 44px); font-weight: 700; color: #ffd9a3;
}
.w-alert {
  background: #241417; border: 2px solid var(--bad); border-radius: 12px; color: #ffb4b4;
  padding: calc(0.9vh * var(--wall-scale)) calc(1.2vw * var(--wall-scale));
  font-size: clamp(12px, calc(1.15vw * var(--wall-scale)), 30px); font-weight: 600;
}

.w-stages { flex: 1; display: grid; gap: calc(1.2vw * var(--wall-scale)); min-height: 0; }
.w-stages.cols-1 { grid-template-columns: 1fr; }
.w-stages.cols-2 { grid-template-columns: repeat(2, 1fr); }
.w-stages.cols-3 { grid-template-columns: repeat(3, 1fr); }
.w-stages:has(> :nth-child(4)) { grid-template-columns: repeat(3, 1fr); grid-auto-rows: 1fr; }

.w-stage {
  background: var(--panel); border: 2px solid var(--panel-border); border-radius: 16px;
  padding: calc(1.8vh * var(--wall-scale)) calc(1.6vw * var(--wall-scale));
  display: flex; flex-direction: column; min-height: 0; overflow: hidden;
}
.w-stages { flex: 0 0 auto; }
.w-stage.live { border-color: var(--accent); background: #14202f; }
.w-stage.live.warn { border-color: var(--warn); background: #2a2312; }
.w-stage.live.over { border-color: var(--bad); background: #241417; }
.w-stage.live.uncovered { border-color: var(--bad); }
.w-stage-head { display: flex; justify-content: space-between; align-items: center; gap: 1vw; }
.w-stage-name {
  font-size: clamp(12px, calc(1.15vw * var(--wall-scale)), 28px); font-weight: 800;
  text-transform: uppercase; letter-spacing: 0.06em;
}
.w-onair {
  font-size: clamp(10px, calc(0.8vw * var(--wall-scale)), 20px); font-weight: 800; letter-spacing: 0.08em;
  background: var(--good); color: #04241a; padding: 0.3em 0.7em; border-radius: 6px; white-space: nowrap;
}
/* The live block sits centred in the panel so a three-up wall doesn't read as
   three headings floating above a lot of nothing. */
/* Never shrink below its content — clipping the restriction line is worse than
   a slightly taller panel. */
.w-mid { flex: 0 0 auto; display: flex; flex-direction: column; justify-content: center; }
.w-artist {
  font-size: clamp(20px, calc(2.4vw * var(--wall-scale)), 60px); font-weight: 800;
  line-height: 1.05; margin-top: 0.4vh; word-break: break-word;
}
.w-star { color: var(--warn); }
.w-times { font-size: clamp(12px, calc(1.05vw * var(--wall-scale)), 26px); color: var(--dim); margin-top: 0.3vh; }
.w-crew { display: flex; flex-wrap: wrap; gap: 0.5vw; margin-top: 0.8vh; }
.w-crew span {
  background: #0f1721; border: 1px solid var(--panel-border); border-radius: 999px;
  padding: 0.25em 0.7em; font-size: clamp(12px, calc(1.15vw * var(--wall-scale)), 30px); font-weight: 700;
}
.w-crew.none {
  color: var(--bad); font-weight: 800; letter-spacing: 0.06em;
  font-size: clamp(12px, calc(1.15vw * var(--wall-scale)), 30px); margin-top: 0.8vh;
}
.w-restrict {
  margin-top: 0.7vh; color: #ffbe7a; font-weight: 700;
  font-size: clamp(11px, calc(1vw * var(--wall-scale)), 24px);
}
.w-bar { margin-top: calc(0.8vh * var(--wall-scale)); height: calc(0.9vh * var(--wall-scale)); min-height: 6px; background: #2a2e38; border-radius: 999px; overflow: hidden; }
.w-bar-fill { height: 100%; background: var(--accent); transition: width 1s linear; }
.w-bar-fill.warn { background: var(--warn); }
.w-bar-fill.over { background: var(--bad); }
.w-remaining {
  margin-top: 0.4vh; font-size: clamp(13px, calc(1.3vw * var(--wall-scale)), 32px);
  font-weight: 800; font-variant-numeric: tabular-nums;
}
.w-remaining.warn { color: var(--warn); }
.w-remaining.over { color: var(--bad); }
.w-idle { color: var(--dim); font-size: clamp(14px, calc(1.5vw * var(--wall-scale)), 36px); font-weight: 600; margin-top: 0.6vh; }
.w-next {
  margin-top: 0.9vh; padding-top: 0.8vh; border-top: 1px solid var(--panel-border);
  font-size: clamp(11px, calc(1vw * var(--wall-scale)), 24px); color: var(--dim);
}
.w-next b { color: var(--text); }
.w-next-label { text-transform: uppercase; letter-spacing: 0.06em; font-weight: 700; margin-right: 0.4em; }
.w-next-warn { color: var(--bad); font-weight: 700; }
.w-nextband { flex: 0 0 auto; }
.w-nextband-label {
  font-size: clamp(10px, calc(0.85vw * var(--wall-scale)), 20px); font-weight: 800;
  letter-spacing: 0.1em; text-transform: uppercase; color: var(--dim);
  margin-bottom: calc(0.8vh * var(--wall-scale));
}
.w-nextband-cards { display: grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); gap: calc(1vw * var(--wall-scale)); }
.w-nextcard {
  display: flex; align-items: center; gap: calc(0.8vw * var(--wall-scale));
  background: #10151f; border: 1px solid var(--panel-border); border-radius: 12px;
  padding: calc(1vh * var(--wall-scale)) calc(1vw * var(--wall-scale)); min-width: 0;
}
.w-nextcard.uncovered { border-color: var(--bad); }
.w-nextcard-when { flex-shrink: 0; text-align: left; }
.w-nextcard-when b {
  display: block; font-size: clamp(13px, calc(1.2vw * var(--wall-scale)), 30px);
  font-weight: 800; font-variant-numeric: tabular-nums;
}
.w-nextcard-when span { font-size: clamp(10px, calc(0.8vw * var(--wall-scale)), 19px); color: var(--dim); white-space: nowrap; }
.w-nextcard-body { flex: 1; min-width: 0; }
.w-nextcard-artist {
  font-size: clamp(13px, calc(1.25vw * var(--wall-scale)), 32px); font-weight: 700;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis; line-height: 1.15;
}
.w-nextcard-meta {
  font-size: clamp(10px, calc(0.85vw * var(--wall-scale)), 20px); color: var(--dim);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}

.w-next-badge {
  font-size: clamp(10px, calc(0.8vw * var(--wall-scale)), 20px); font-weight: 800; letter-spacing: 0.08em;
  background: #23304a; color: #9dc0ff; padding: 0.3em 0.7em; border-radius: 6px; white-space: nowrap;
}
/* a live set with nobody on it, in shooting mode */
.w-stage.live.uncovered .w-onair { background: var(--bad); color: #fff; }

.w-up {
  display: flex; align-items: center; gap: 0.6vw;
  background: var(--panel); border: 1px solid var(--panel-border); border-radius: 10px;
  padding: calc(0.7vh * var(--wall-scale)) calc(0.7vw * var(--wall-scale)); min-width: 0;
}
.w-up.uncovered { border-color: var(--bad); }
.w-up-time { font-weight: 800; font-variant-numeric: tabular-nums; font-size: clamp(12px, calc(1.05vw * var(--wall-scale)), 26px); white-space: nowrap; }
.w-up-body { flex: 1; min-width: 0; }
.w-up-artist { font-weight: 700; font-size: clamp(12px, calc(1.05vw * var(--wall-scale)), 26px); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.w-up-meta { font-size: clamp(10px, calc(0.85vw * var(--wall-scale)), 20px); color: var(--dim); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.w-up-meta .none { color: var(--bad); font-weight: 700; }
.w-up-in { font-size: clamp(10px, calc(0.85vw * var(--wall-scale)), 20px); color: var(--dim); white-space: nowrap; font-variant-numeric: tabular-nums; }

/* Stages-across-the-day band. Positions are percentages of a moving time window
   rather than pixels per minute, so it always fits the screen — a wall display
   can't be scrolled. */
.w-timeline {
  flex: 1 1 auto; min-height: 0; margin-top: calc(0.8vh * var(--wall-scale));
  display: flex; flex-direction: column;
}
.wt-head { display: flex; align-items: center; height: calc(2vh * var(--wall-scale)); min-height: 15px; flex: 0 0 auto; }
.wt-head .wt-track { position: relative; height: 100%; }
.wt-tick { position: absolute; top: 0; bottom: 0; border-left: 1px solid #262a34; }
.wt-tick span {
  position: absolute; left: 3px; top: 0; color: var(--dim); white-space: nowrap;
  font-size: clamp(8px, calc(0.68vw * var(--wall-scale)), 16px);
}
.wt-lane {
  display: flex; align-items: stretch; gap: calc(0.4vw * var(--wall-scale));
  margin-top: calc(0.4vh * var(--wall-scale));
  flex: 1 1 0; min-height: 46px; /* lanes share the leftover height evenly */
}
.wt-name {
  flex-shrink: 0; width: calc(7vw * var(--wall-scale)); min-width: 70px; max-width: 150px;
  display: flex; align-items: center; font-weight: 800; text-transform: uppercase;
  letter-spacing: 0.05em; font-size: clamp(9px, calc(0.78vw * var(--wall-scale)), 18px);
}
.wt-track {
  position: relative; flex: 1; height: 100%; min-height: 46px;
  background: #14171d; border-radius: 6px; overflow: hidden;
}
.wt-block {
  position: absolute; top: 2px; bottom: 2px; border-radius: 5px; overflow: hidden;
  padding: 0 0.4vw; background: var(--panel); border: 1px solid var(--panel-border);
  border-left-width: 3px; display: flex; flex-direction: column; justify-content: center;
}
.wt-block.covered { border-left-color: var(--good); }
.wt-block.partial { border-left-color: var(--warn); }
.wt-block.uncovered { border-left-color: var(--bad); background: #241417; }
.wt-block.live { border-color: var(--accent); border-left-color: var(--accent); background: #14202f; }
.wt-block.done { opacity: 0.45; }
.wt-artist {
  font-weight: 700; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
  font-size: clamp(10px, calc(0.95vw * var(--wall-scale)), 23px); line-height: 1.2;
}
.wt-crew {
  color: var(--dim); white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
  font-size: clamp(9px, calc(0.78vw * var(--wall-scale)), 19px);
}
.wt-block.uncovered .wt-crew { color: var(--bad); font-weight: 700; }
.wt-now { position: absolute; top: 0; bottom: 0; width: 2px; background: var(--bad); z-index: 2; }

/* Plot on the wall: the map fills whatever space is left under the header. */
.w-plot-stage { flex: 1; min-height: 0; display: flex; align-items: center; justify-content: center; }
.w-plot-wrap { position: relative; display: inline-block; max-width: 100%; max-height: 100%; line-height: 0; }
.w-plot-wrap img { max-width: 100%; max-height: 78vh; width: auto; height: auto; display: block; border-radius: 10px; }

.w-empty { height: 100%; display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 1vh; }
.w-empty-title { font-size: clamp(20px, 3vw, 60px); font-weight: 700; }
.w-empty-sub { color: var(--dim); font-size: clamp(13px, 1.3vw, 28px); }

.wall-controls {
  position: fixed; bottom: 8px; right: 8px; transition: opacity 0.6s;
  background: rgba(15,17,21,0.85); border-radius: 10px; padding: 4px;
}
.wall-controls.hidden { opacity: 0; pointer-events: none; }

/* ---- Printed day sheet ---- */
#print-area { display: none; }

@media print {
  @page { size: landscape; margin: 10mm; }
  body { background: #fff; color: #000; }
  .app { display: none !important; }
  .no-print { display: none !important; }
  .print-only { display: block; }
  #print-area { display: block !important; }
  .card, .panel { border-color: #ccc; background: #fff; color: #000; }
  a { color: #000; }

  .sheet { font-size: 9.5pt; color: #000; }
  .sheet.page-break { break-before: page; page-break-before: always; }
  .sheet-head { border-bottom: 2px solid #000; padding-bottom: 5px; margin-bottom: 8px; }
  .sheet-head h1 { font-size: 14pt; margin: 0 0 2px; }
  .sheet .muted { color: #555; font-weight: 400; }
  .sheet-notice { border: 1.5px solid #000; padding: 5px 7px; margin-bottom: 8px; font-weight: 600; }

  /* The runsheet grids: time down the side, a column per shooter or per stage.
     A set spanning several time rows is drawn once with a rowspan, so the block
     is as tall as the set is long — the shape of the day is the point. */
  table.grid { width: 100%; border-collapse: collapse; table-layout: fixed; }
  table.grid thead { display: table-header-group; } /* repeat the names on every page */
  table.grid th {
    text-align: left; font-size: 8pt; text-transform: uppercase; letter-spacing: 0.04em;
    border: 1px solid #000; padding: 4px 5px; vertical-align: bottom;
  }
  table.grid th.t-col, table.grid td.t-col { width: 52px; }
  table.grid .col-sub { font-size: 6.5pt; font-weight: 400; text-transform: none; letter-spacing: 0; color: #555; }
  table.grid td { border: 1px solid #bbb; padding: 3px 5px; vertical-align: top; }
  table.grid td.t-col {
    border-left: 1px solid #000; border-right: 1px solid #000;
    font-size: 8pt; font-weight: 700; white-space: nowrap; color: #000;
  }
  table.grid td.gap { background: repeating-linear-gradient(135deg, #fff, #fff 4px, #f2f2f2 4px, #f2f2f2 8px); }
  table.grid td.filled { border: 1px solid #000; }
  table.grid td.filled.is-break { background: #eee; font-style: italic; }
  table.grid td.filled.uncovered { background: #e4e4e4; }
  table.grid .c-title { font-weight: 700; font-size: 9pt; line-height: 1.15; }
  table.grid .c-sub { font-size: 7.5pt; color: #444; margin-top: 1px; }
  table.grid sup { font-size: 6.5pt; }

  .footnotes { margin-top: 10px; font-size: 7.5pt; line-height: 1.45; break-inside: avoid; }
  .footnotes b { display: block; text-transform: uppercase; letter-spacing: 0.05em; font-size: 7pt; margin-bottom: 2px; }
}

/* ---------- delivery: the planner's list ---------- */
.dlv-head { display: flex; align-items: center; gap: 12px; margin-bottom: 14px; }
.dlv-head h2 { margin: 0; }
.dlv-count { font-size: 13px; color: var(--dim); }
.dlv-count b { color: var(--text); font-size: 16px; }
.dlv-head a { margin-left: auto; }

.dlv-list { display: flex; flex-direction: column; gap: 1px; background: var(--panel-border); border: 1px solid var(--panel-border); border-radius: 8px; overflow: hidden; }
.dlv-item { display: flex; align-items: center; gap: 14px; padding: 10px 14px; background: var(--panel); }
.dlv-item:hover { background: var(--panel-3); }

/* Fixed-width so the pills form a column the eye can run down, rather than
 * jittering with the length of each state's name. */
.dlv-pill {
  flex: 0 0 92px; text-align: center; border-radius: 999px; padding: 2px 0;
  border: 2px solid transparent;
  font-size: 11px; font-weight: 700; text-transform: uppercase; letter-spacing: 0.04em;
  color: #0b0e13;
}
/* Matches the wall: outlined rather than solid where a set is part-done. */
.dlv-when { flex: 0 0 64px; font-size: 13px; color: var(--dim); font-variant-numeric: tabular-nums; }
.dlv-who { flex: 1 1 auto; min-width: 0; display: flex; flex-direction: column; line-height: 1.3; }
.dlv-who b { font-size: 14px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.dlv-who i { font-style: normal; font-size: 12px; color: var(--dim); }
.dlv-meta { flex: 0 1 auto; display: flex; gap: 10px; align-items: center; font-size: 12px; color: var(--dim); text-align: right; }
.dlv-meta em { font-style: normal; color: var(--warn); }
.dlv-link { flex: 0 0 76px; text-align: right; font-size: 12px; white-space: nowrap; }
.dlv-link.none { color: var(--panel-border); }
.dlv-empty { color: var(--dim); padding: 24px 0; }

@media (max-width: 700px) {
  .dlv-item { flex-wrap: wrap; gap: 8px 12px; }
  .dlv-who { flex: 1 1 100%; order: -1; }
  .dlv-meta { text-align: left; }
}

/* ---------- delivery: the wall board ---------- */
.wdlv-head { font-size: 30px; font-weight: 700; margin: 0 0 6px; }
.wdlv-summary { font-size: 22px; color: var(--dim); margin-bottom: 20px; }
.wdlv-summary b { color: var(--text); font-size: 32px; }
.wdlv-board { display: grid; gap: 14px; align-items: start; }
.wdlv-col { display: flex; flex-direction: column; gap: 8px; min-width: 0; }
.wdlv-stage {
  font-weight: 700; font-size: 17px; letter-spacing: 0.06em; text-transform: uppercase;
  color: var(--dim); padding-bottom: 8px; border-bottom: 1px solid var(--panel-border);
}
.wdlv-cell {
  border-radius: 6px; overflow: hidden;
  border: 3px solid transparent;
  padding: 6px 9px; display: flex; flex-direction: column; gap: 1px;
  color: #0b0e13; font-size: 17px; line-height: 1.25;
}
/* Strong edge, paler middle — a set whose footage is in but not out reads as
 * part-done beside the solid blocks either side of it. Text stays dark: the
 * interior is lighter than the solid states, not darker. */
.wdlv-cell.is-part .wdlv-time { opacity: 0.8; }
/* Pending's grey sits close to the page background, so the dark text used on
 * the bright states vanishes into it. */
.wdlv-cell.is-pending { color: var(--text); }
.wdlv-artist { font-weight: 700; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.wdlv-time { font-size: 14px; opacity: 0.7; font-variant-numeric: tabular-nums; }
.wdlv-cell.is-pending .wdlv-time { opacity: 0.85; }
.wdlv-cards { margin-left: 8px; }
.wdlv-empty { color: var(--dim); font-size: 22px; padding: 30px 0; }

/* Sits under the board rather than above it: the board is what the room looks
 * at, and the key is what you read once and stop needing. */
.wdlv-key {
  display: flex; flex-wrap: wrap; gap: 12px 28px;
  margin-top: 26px; padding-top: 18px;
  border-top: 1px solid var(--panel-border);
  font-size: 17px; color: var(--dim);
}
.wdlv-key-item { display: flex; align-items: center; gap: 9px; }
.wdlv-key-dot { width: 26px; height: 18px; border-radius: 4px; border: 3px solid transparent; flex: none; }


/* ---------- timecode marks on the crew page ----------
 * A shooter taps this with one hand, in the dark, while holding a camera.
 * So: big enough to hit without looking, obvious when it has registered, and
 * honest when a mark is still sitting in the queue waiting for signal. */
.mark-row { display: flex; align-items: center; gap: 10px; margin-top: 10px; flex-wrap: wrap; }
.mark-btn {
  display: inline-flex; align-items: center; gap: 8px;
  min-height: 44px; padding: 0 16px;
  border: 1px solid var(--panel-border); border-radius: 8px;
  background: #1b2130; color: var(--text);
  font: inherit; font-size: 14px; font-weight: 600;
  cursor: pointer; -webkit-tap-highlight-color: transparent;
  touch-action: manipulation;
}
.mark-btn .mark-dot {
  width: 9px; height: 9px; border-radius: 50%;
  background: var(--dim); flex: none;
}
.mark-btn.live { border-color: var(--bad); background: #2a1a1e; }
.mark-btn.live .mark-dot { background: var(--bad); }
.mark-btn.past { opacity: .72; }
.mark-btn:active { transform: translateY(1px); }
.mark-pending { font-size: 12px; color: var(--dim); }
.mark-status {
  margin-top: 6px; font-size: 12px; color: var(--dim);
}
/* The day's marks, counted once at the head of the call sheet. Sits between
   the "Your day" heading and the first card, so it reads as a summary of the
   list rather than as another item in it — no card chrome, no border, just a
   line. */
.marks-today {
  margin: -2px 0 12px; font-size: 12.5px; color: var(--dim); line-height: 1.5;
}
.marks-today b { color: var(--text); font-weight: 600; }
@media (prefers-reduced-motion: no-preference) {
  .mark-btn { transition: background .12s ease, border-color .12s ease, transform .06s ease; }
}
@media print { .mark-row, .mark-status { display: none; } }


/* ---------- camera plot page (plot.html) ----------
 * A map that fills the window, with the schedule's chrome cut back to a single
 * bar: on site this is held one-handed in daylight, and everything that isn't
 * the plot is in the way. The map and its overlay share one transformed box,
 * so they cannot drift apart from each other however far it is zoomed.
 */
body.plot-body {
  height: 100%;
  overflow: hidden;       /* the map pans; the page does not scroll */
  overscroll-behavior: none;
}
/* Whatever a drag passes over, it is a drag — see the selectstart guard. */
body.plot-dragging { user-select: none; -webkit-user-select: none; }
body.plot-dragging .plot-stage { cursor: grabbing; }

.plot-top {
  position: fixed; top: 0; left: 0; right: 0; z-index: 4;
  display: flex; align-items: center; gap: 10px;
  padding: calc(10px + env(safe-area-inset-top, 0px)) 12px 10px;
  background: linear-gradient(180deg, rgba(15, 17, 21, 0.94), rgba(15, 17, 21, 0.72) 70%, rgba(15, 17, 21, 0));
  pointer-events: none;   /* the map keeps the whole window; only the controls take clicks */
}
.plot-top > * { pointer-events: auto; }
.plot-back {
  flex: none; width: 34px; height: 34px; border-radius: 8px;
  display: flex; align-items: center; justify-content: center;
  background: var(--panel); border: 1px solid var(--panel-border);
  color: var(--dim); text-decoration: none; font-size: 16px;
}
.plot-back:hover { color: var(--text); border-color: var(--accent); }
.plot-titles { min-width: 0; flex: 1; }
.plot-name {
  font-size: 15px; font-weight: 600;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.plot-sub { font-size: 12px; color: var(--dim); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

/* Deliberately not a .sync-pill: that one says whether YOUR edits are saved,
   and this page has no edits. This says how the plot in front of you is being
   kept current, which is a different question with a different worst case. */
.plot-conn {
  flex: none;
  font-size: 10px; font-weight: 700; letter-spacing: 0.04em; text-transform: uppercase;
  padding: 4px 9px; border-radius: 999px;
  border: 1px solid var(--panel-border); color: var(--dim); white-space: nowrap;
}
.plot-conn.live { color: var(--good); border-color: #21503c; }
.plot-conn.polling { color: var(--dim); }
.plot-conn.stale { color: var(--bad); border-color: var(--bad); }
.plot-conn.demo { color: var(--accent); border-color: var(--accent); }

/* Fit is the one-handed way back, and it has to be reachable with a thumb.
   Double-tap on the map is a ping now, and double-tap OFF the map only helps
   while there is map-free backdrop to hit — past about 1.4x zoom the map fills
   the screen and this button is the only escape left, which is exactly when
   somebody needs it.

   Measured 47x37 at 375px, which fails the 44x44 minimum on height. The target
   is grown with a pseudo-element rather than with padding so the button's own
   size, the bar's height and the layout around it are all untouched — padding
   here would push the top bar down and move the thing it is trying to fix.

   Vertical only: 47px is already over the minimum horizontally, and growing it
   sideways would eat the 10px gap to Edit/Unlock and start stealing their taps.
   The band stays inside the bar's own 57px, so it takes nothing from the map
   beyond a few pixels directly above and below a button that is already there. */
#plot-fit { position: relative; }
#plot-fit::after {
  content: "";
  position: absolute;
  left: 0; right: 0;
  top: 50%;
  transform: translateY(-50%);
  height: 44px;
}
/* button.small (near the top of this file) sets its own 9px/14px padding on
 * every small button including this one, at higher specificity than a bare
 * class — so `.plot-utility-btn { padding: 0 }` alone never actually won,
 * and every icon square built from this class has been sitting in a content
 * box smaller and off-centre from the 34px square it looks like, not just
 * the zoom row that made it obvious. Repeating the type selector here
 * matches that specificity and wins on source order instead. */
button.plot-utility-btn { width: 34px; min-width: 34px; height: 34px; padding: 0; display: inline-grid; place-items: center; }
.plot-utility-label { display: none; }

.plot-stage {
  position: fixed; inset: 0;
  overflow: hidden;
  background: #0b0d11;
  touch-action: none;     /* the page owns pan and pinch */
  cursor: grab;
  -webkit-tap-highlight-color: transparent;
}
/* No map decoded yet: the <img> has no src, and a src-less <img> with alt text
   draws Chrome's broken-image glyph in the corner of an otherwise empty page. */
.plot-stage.is-blank { cursor: default; }
.plot-stage.is-blank .plot-pan { display: none; }
.plot-pan {
  position: absolute; top: 0; left: 0;
  transform-origin: 0 0;
  will-change: transform;
}
.plot-map { display: block; width: 100%; height: auto; user-select: none; -webkit-user-drag: none; transition: filter .16s ease; }
body.plot-has-selection .plot-map { filter: brightness(.72); }
/* Between the map and the overlay in the DOM (see plot.html), which is what
 * puts it between them in the paint order too — no z-index needed, and none
 * added, so nothing here can end up wrongly above the pins. Colour lives in
 * its own variable, defaulting to black, so a future "lighten" direction for
 * a dark map is a value swapped in here rather than a different mechanism. */
.plot-scrim { position: absolute; inset: 0; background: var(--plot-scrim-color, #000); opacity: 0; pointer-events: none; }
.plot-overlay { position: absolute; inset: 0; }
.plot-overlay .plot-svg { display: block; width: 100%; height: 100%; overflow: visible; }

/* ---- layer toggles ----
 * Classes on the overlay container, toggled from plot.html's applyLayers()
 * rather than rebuilding the SVG — renderPlotSvg already draws these seams
 * (`plot-line`/`plot-moves`/`plot-labels`, a `kind-*` on every item and
 * label), so hiding a layer is display:none on markup that's already there. */
.plot-overlay.hide-routing .plot-line,
.plot-overlay.hide-routing .plot-line-label,
.plot-overlay.hide-routing .plot-wire-handles { display: none; }
.plot-overlay.hide-moves .plot-moves,
.plot-overlay.hide-moves .plot-move-handles { display: none; }
.plot-overlay.hide-labels .plot-labels { display: none; }
.plot-overlay.hide-shapes .plot-shapes { display: none; }
.plot-overlay.hide-kind-camera .kind-camera,
.plot-overlay.hide-kind-receiver .kind-receiver,
.plot-overlay.hide-kind-drone .kind-drone,
.plot-overlay.hide-kind-monitor .kind-monitor,
.plot-overlay.hide-kind-recorder .kind-recorder,
.plot-overlay.hide-kind-switch .kind-switch,
.plot-overlay.hide-kind-fibre .kind-fibre,
.plot-overlay.hide-kind-audio .kind-audio,
.plot-overlay.hide-kind-power .kind-power,
.plot-overlay.hide-kind-marker .kind-marker { display: none; }
/* Finding 4 of the final branch review: hiding a kind hid that kind's own pin
 * and label (the rule above) but left every wire touching it, and its move
 * ribbon, drawn with nothing at either end. A wire carries both endpoints'
 * kinds as its own data-from-kind/data-to-kind (renderPlotSvg, app.js), so
 * either side being hidden takes the wire and its custom label with it. A
 * move ribbon needs no such attribute: renderPlotSvg only ever draws one for
 * a camera, so .hide-kind-camera alone is the whole rule for it.
 *
 * A wire's own waypoint handles carry the same two attributes (addWireHandles)
 * and are added here for the same reason, round 3's review Important 2: a
 * grip on an otherwise-invisible wire was left fully interactive — visible to
 * nobody, grabbable by anybody who happened to press where it used to be. */
.plot-overlay.hide-kind-camera .plot-line[data-from-kind="camera"],
.plot-overlay.hide-kind-camera .plot-line[data-to-kind="camera"],
.plot-overlay.hide-kind-camera .plot-line-label[data-from-kind="camera"],
.plot-overlay.hide-kind-camera .plot-line-label[data-to-kind="camera"],
.plot-overlay.hide-kind-camera .plot-wire-handle[data-from-kind="camera"],
.plot-overlay.hide-kind-camera .plot-wire-handle[data-to-kind="camera"],
.plot-overlay.hide-kind-camera .plot-move,
.plot-overlay.hide-kind-receiver .plot-line[data-from-kind="receiver"],
.plot-overlay.hide-kind-receiver .plot-line[data-to-kind="receiver"],
.plot-overlay.hide-kind-receiver .plot-line-label[data-from-kind="receiver"],
.plot-overlay.hide-kind-receiver .plot-line-label[data-to-kind="receiver"],
.plot-overlay.hide-kind-receiver .plot-wire-handle[data-from-kind="receiver"],
.plot-overlay.hide-kind-receiver .plot-wire-handle[data-to-kind="receiver"],
.plot-overlay.hide-kind-drone .plot-line[data-from-kind="drone"],
.plot-overlay.hide-kind-drone .plot-line[data-to-kind="drone"],
.plot-overlay.hide-kind-drone .plot-line-label[data-from-kind="drone"],
.plot-overlay.hide-kind-drone .plot-line-label[data-to-kind="drone"],
.plot-overlay.hide-kind-drone .plot-wire-handle[data-from-kind="drone"],
.plot-overlay.hide-kind-drone .plot-wire-handle[data-to-kind="drone"],
.plot-overlay.hide-kind-monitor .plot-line[data-from-kind="monitor"],
.plot-overlay.hide-kind-monitor .plot-line[data-to-kind="monitor"],
.plot-overlay.hide-kind-monitor .plot-line-label[data-from-kind="monitor"],
.plot-overlay.hide-kind-monitor .plot-line-label[data-to-kind="monitor"],
.plot-overlay.hide-kind-monitor .plot-wire-handle[data-from-kind="monitor"],
.plot-overlay.hide-kind-monitor .plot-wire-handle[data-to-kind="monitor"],
.plot-overlay.hide-kind-recorder .plot-line[data-from-kind="recorder"],
.plot-overlay.hide-kind-recorder .plot-line[data-to-kind="recorder"],
.plot-overlay.hide-kind-recorder .plot-line-label[data-from-kind="recorder"],
.plot-overlay.hide-kind-recorder .plot-line-label[data-to-kind="recorder"],
.plot-overlay.hide-kind-recorder .plot-wire-handle[data-from-kind="recorder"],
.plot-overlay.hide-kind-recorder .plot-wire-handle[data-to-kind="recorder"],
.plot-overlay.hide-kind-switch .plot-line[data-from-kind="switch"],
.plot-overlay.hide-kind-switch .plot-line[data-to-kind="switch"],
.plot-overlay.hide-kind-switch .plot-line-label[data-from-kind="switch"],
.plot-overlay.hide-kind-switch .plot-line-label[data-to-kind="switch"],
.plot-overlay.hide-kind-switch .plot-wire-handle[data-from-kind="switch"],
.plot-overlay.hide-kind-switch .plot-wire-handle[data-to-kind="switch"],
.plot-overlay.hide-kind-fibre .plot-line[data-from-kind="fibre"],
.plot-overlay.hide-kind-fibre .plot-line[data-to-kind="fibre"],
.plot-overlay.hide-kind-fibre .plot-line-label[data-from-kind="fibre"],
.plot-overlay.hide-kind-fibre .plot-line-label[data-to-kind="fibre"],
.plot-overlay.hide-kind-fibre .plot-wire-handle[data-from-kind="fibre"],
.plot-overlay.hide-kind-fibre .plot-wire-handle[data-to-kind="fibre"],
.plot-overlay.hide-kind-audio .plot-line[data-from-kind="audio"],
.plot-overlay.hide-kind-audio .plot-line[data-to-kind="audio"],
.plot-overlay.hide-kind-audio .plot-line-label[data-from-kind="audio"],
.plot-overlay.hide-kind-audio .plot-line-label[data-to-kind="audio"],
.plot-overlay.hide-kind-audio .plot-wire-handle[data-from-kind="audio"],
.plot-overlay.hide-kind-audio .plot-wire-handle[data-to-kind="audio"],
.plot-overlay.hide-kind-power .plot-line[data-from-kind="power"],
.plot-overlay.hide-kind-power .plot-line[data-to-kind="power"],
.plot-overlay.hide-kind-power .plot-line-label[data-from-kind="power"],
.plot-overlay.hide-kind-power .plot-line-label[data-to-kind="power"],
.plot-overlay.hide-kind-power .plot-wire-handle[data-from-kind="power"],
.plot-overlay.hide-kind-power .plot-wire-handle[data-to-kind="power"],
.plot-overlay.hide-kind-marker .plot-line[data-from-kind="marker"],
.plot-overlay.hide-kind-marker .plot-line[data-to-kind="marker"],
.plot-overlay.hide-kind-marker .plot-line-label[data-from-kind="marker"],
.plot-overlay.hide-kind-marker .plot-line-label[data-to-kind="marker"],
.plot-overlay.hide-kind-marker .plot-wire-handle[data-from-kind="marker"],
.plot-overlay.hide-kind-marker .plot-wire-handle[data-to-kind="marker"] { display: none; }

/* ---- what is drawn on the map ---- */

.plot-line {
  fill: none;
  stroke-width: var(--linew, 2);
  stroke-linecap: round;
  opacity: 0.85;
}
.plot-line.is-wireless { stroke-dasharray: calc(var(--linew, 2) * 3) calc(var(--linew, 2) * 2.4); }
/* Colour is carried by the FAMILY — one glance says "that is a power run" —
   so where a family holds two types the dash is what tells them apart: comms
   from audio, timecode from network. Video is the exception and keeps its four
   learned colours, which is why nothing here dashes sdi, fibre or ndi. */
.plot-line.type-comms { stroke-dasharray: calc(var(--linew, 2) * 3) calc(var(--linew, 2) * 2.4); }
.plot-line.type-timecode { stroke-dasharray: 0 calc(var(--linew, 2) * 2.6); }
.plot-line.type-custom { stroke-dasharray: calc(var(--linew, 2) * 6) calc(var(--linew, 2) * 2.4); }

/* ---- signal flow: a moving pulse reading as current on the wire ----
 *
 * renderPlotSvg (app.js) draws this as a SECOND path over the exact same `d`
 * as the wire above, not as a second stroke-dasharray layered onto `.plot-line`
 * itself — every rule just above spends that property saying which TYPE of
 * link this is (comms vs audio, timecode vs network, wireless's own dashed
 * bow), and an animation sharing it would either scroll that meaning out from
 * under itself or have to blank it first. A second path can't drift off the
 * real line (same `d`, computed once) and never touches stroke-dasharray on
 * an element that already means something with it, so it composes with
 * every dash style above instead of fighting any of them — confirmed by
 * toggling this on over a wireless run and a comms run in the browser, not
 * just reasoned about.
 *
 * It carries `.plot-line` as well as `.plot-line-flow` so the hide-routing
 * and hide-kind-* rules above (which key off `.plot-line` plus the same
 * data-from-kind/data-to-kind this path repeats) hide the pulse right along
 * with the wire it belongs to — one seam, not two copies of it. What it does
 * NOT carry is a `type-*` class, which is exactly the dasharray it stays out
 * of; its own dash pattern below is deliberately unrelated to any of theirs. */
.plot-line-flow {
  stroke-width: calc(var(--linew, 2) * 1.4);
  stroke-dasharray: calc(var(--linew, 2) * 1.2) calc(var(--linew, 2) * 5);
  opacity: 0;
  pointer-events: none;
}
.plot-overlay:not(.hide-signal-flow) .plot-line-flow { opacity: 0.9; }
@media (prefers-reduced-motion: no-preference) {
  .plot-overlay:not(.hide-signal-flow) .plot-line-flow {
    /* 1.3s a cycle: fast enough to read as something moving rather than a
     * static dash, slow enough not to read as flicker — a starting point for
     * a future tweak, not a measured constant. */
    animation: plot-signal-flow 1.3s linear infinite;
  }
}
/* Reduced motion drops the animation above, but the base rule two rules up
 * still gives the path opacity: 0.9 on its own — left there, every wire on
 * the plot would carry a permanent static dash nobody asked for, which is
 * worse than either the motion someone opted out of or no change at all. Same
 * selector as that base rule, so this wins on source order rather than
 * needing !important, and hides the path outright instead. */
@media (prefers-reduced-motion: reduce) {
  .plot-overlay:not(.hide-signal-flow) .plot-line-flow { opacity: 0; }
}
/* plotLinks/renderPlotSvg (app.js) always build `d` source -> target: `a` is
 * link.from's pin, `b` is link.to's, for a straight run, a hand-routed one
 * and the wireless bow alike (confirmed by direct reading, not assumed). A
 * NEGATIVE stroke-dashoffset is what reads as the dash sliding the same
 * direction the path was drawn in — a positive offset shifts the pattern
 * backward along the path's own parameterisation instead, which looked like
 * current flowing from the receiver back to the camera when checked in the
 * browser. The distance travelled per cycle (6.2 * linew) is exactly one
 * dash-plus-gap from the rule above, so the loop has no visible seam. */
@keyframes plot-signal-flow {
  /* The `px` here is load-bearing, unlike every unitless calc() elsewhere in
   * this file: stroke-dasharray and stroke-width are static, so an SVG
   * geometry property happily takes a bare number as a user unit. An
   * ANIMATED property is different — confirmed in the browser, not assumed —
   * a unitless calc() target still applies as a static value but the CSS
   * animation engine won't interpolate it smoothly, since it can't be
   * recognised as a <length>: sampled mid-animation it held at 0 then
   * snapped straight to the end value partway through, never moving in
   * between. `px` here and 1 SVG user unit are the same size in this
   * viewBox (#plot-pan is the map's own natural CSS-pixel size — see the
   * pan/zoom header above), so the loop distance still matches the dasharray
   * cycle exactly. */
  to { stroke-dashoffset: calc(var(--linew, 2) * -6.2px); }
}

/* A custom wire is the only one whose colour and dash name nothing, so it
   writes what it is on the map — backed the same way an item's label is,
   because the map under it is usually a photograph. */
.plot-line-label text {
  font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
  font-size: 10px; font-weight: 600; fill: var(--dim);
  paint-order: stroke fill;
  stroke: #0b0d11; stroke-width: 3; stroke-linejoin: round;
}
.plot-line-label { pointer-events: none; }

/* A stage or barrier outline: a third kind of mark on the map, and it has to
   read as neither of the other two. A wire is a thin crisp saturated line
   between two pins; a move is a broad faint ribbon leaving one; an outline is
   a closed figure with a wash of fill and a firm edge, in a desaturated tint
   that reads as ground rather than as signal. The fill is barely there on
   purpose — a stage covering a quarter of the map at any real opacity would
   bury the very pins standing on it, which is the opposite of what the
   darkened-map case this was drawn for is asking for.

   The wide invisible stroke underneath is the hit target, and pointer-events
   is on the STROKE alone: the interior belongs to the map, so a pan started
   inside a stage stays a pan. */
.plot-shape-path {
  fill-opacity: 0.1;
  stroke-width: calc(var(--linew, 2) * 1.6);
  stroke-linejoin: round;
  opacity: 0.85;
  /* Not a hit target, and this line is load-bearing: SVG's default is
     `visiblePainted`, and a fill at 10% is still painted, so without it a
     press anywhere inside a stage footprint selected the outline instead of
     panning the map. Confirmed in the browser, not reasoned about — a tap
     well clear of every edge opened the editor. The wide transparent stroke
     below is the only thing here a finger can reach. */
  pointer-events: none;
}
.plot-shape-hit {
  stroke: transparent;
  stroke-width: calc(var(--linew, 2) * 9);
  stroke-linejoin: round;
  pointer-events: stroke;
}
/* Colour says which kind, and the dash says it again for anyone reading a
   printed plot in black and white — the same belt-and-braces the wire
   families already use, and the same reason. A barrier carries no fill at
   all: it is a line you do not cross, not a piece of ground with an inside. */
.plot-shape.shape-barrier .plot-shape-path { fill-opacity: 0; stroke-dasharray: calc(var(--linew, 2) * 5) calc(var(--linew, 2) * 3); }
.plot-shape.shape-zone .plot-shape-path { fill-opacity: 0.06; stroke-dasharray: 0 calc(var(--linew, 2) * 3.4); stroke-linecap: round; }
.plot-shape.is-selected .plot-shape-path {
  opacity: 1;
  stroke-width: calc(var(--linew, 2) * 2.6);
  filter: drop-shadow(0 0 3px var(--accent));
}
/* Named where the shape sits, and backed like every other caption on this map
   — the ground underneath is usually a photograph. Smaller and quieter than an
   item's label: it names a piece of the site, not a thing standing on it. */
.plot-shape-label { pointer-events: none; }
.plot-shape-label text {
  font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
  font-size: 12px; font-weight: 700; fill: var(--dim);
  letter-spacing: 0.06em; text-transform: uppercase;
  paint-order: stroke fill;
  stroke: #0b0d11; stroke-width: 3.5; stroke-linejoin: round;
}

/* A camera's move is not a cable and must never be mistaken for one. Both are
   lines on the same map, so they are told apart by weight and confidence: a
   signal run is thin, crisp and near-solid in a colour that names it, while
   a move is a broad, faint ribbon in the camera's own tint that the eye reads
   as ground it covers rather than a connection to something. The arrowheads
   are the part that has to survive the fading, so they sit at a much higher
   opacity than the ribbon they mark. */
.plot-move-path {
  fill: none;
  stroke-width: calc(var(--linew, 2) * 2.6);
  stroke-linecap: round;
  stroke-linejoin: round;
  opacity: 0.45;
}
.plot-move-arrow {
  fill: none;
  stroke-width: calc(var(--linew, 2) * 1.1);
  stroke-linecap: round;
  stroke-linejoin: round;
  opacity: 0.8;
}
.plot-move-end { stroke: none; opacity: 0.9; }

/* Counter-scaled about the item's own anchor so its position never moves.
   transform-origin is set explicitly because the initial value for an SVG
   element is the middle of the whole viewBox, which would fling every item
   across the map the moment --inv left 1. */
.plot-item-scale {
  transform: scale(var(--inv, 1));
  transform-origin: 0 0;
  transform-box: view-box;
}
/* Labels are painted over every item, so without this they would sit between a
   finger and the disc it is aiming at. The disc is the hit target — it is the
   thing counter-scaling keeps a finger wide — and dragging an item by its
   caption would be a strange way to move a camera anyway. */
.plot-labels { pointer-events: none; }
.plot-disc { stroke: #0b0d11; stroke-width: 1.5; }
.plot-icon { fill: none; stroke: var(--text); stroke-width: 1.7; stroke-linecap: round; stroke-linejoin: round; }
.plot-wedge { opacity: 0.25; }
.plot-ring { stroke: transparent; stroke-width: 2.4; }
.plot-cluster { cursor: zoom-in; pointer-events: auto; }
.plot-cluster-disc { fill: #17243a; stroke: var(--accent); stroke-width: 2.4; }
.plot-cluster-count {
  fill: white; stroke: #0b0d11; stroke-width: 3; paint-order: stroke fill;
  font: 700 11px -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
  text-anchor: middle;
}
.plot-overlay.has-selection .plot-cluster { opacity: .3; }
/* A camera flagged wireless with no receiver wired to it — a checklist nudge,
   not an error, so it borrows --warn (the same amber the sync pill uses for
   "in progress") rather than --bad, and a dashed ring rather than a filled
   dot: the dash is the same shorthand a wireless line itself draws in
   (.plot-line.is-wireless), read here as "not finished" before the colour
   says anything at all. */
.plot-wireless-gap-badge circle { stroke: var(--warn); stroke-width: 2; stroke-dasharray: 3 2.4; }
.plot-wireless-gap-badge { pointer-events: none; }

.plot-label-text, .plot-label-sub {
  font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
  text-anchor: middle;
  /* A site map is usually a photograph or a pale PDF export, so the label
     carries its own dark backing rather than trusting whatever is under it. */
  paint-order: stroke fill;
  stroke: #0b0d11;
  stroke-width: 3.5;
  stroke-linejoin: round;
}
.plot-label-text { font-size: 13px; font-weight: 700; fill: var(--text); }
.plot-label-sub { font-size: 11px; font-weight: 600; fill: var(--dim); }

/* The person reading this on their own phone gets their own positions ringed —
   the one bit of colour the plot spends on "this is about you". */
.plot-item.is-mine .plot-ring { stroke: var(--good); }
.plot-item.is-selected .plot-ring { stroke: var(--accent); }
/* Selection should read at a glance on touch screens: the chosen object stays
   untouched while the rest of the plot steps back. */
.plot-overlay.has-selection .plot-item:not(.is-selected),
.plot-overlay.has-selection .plot-line:not(.is-selected),
.plot-overlay.has-selection .plot-move:not(.is-selected),
.plot-overlay.has-selection .plot-shape:not(.is-selected) {
  opacity: .3;
  transition: opacity .16s ease;
}
/* The ring above marks the pin; these two mark what it is attached to — the
   same `is-selected` class renderPlotSvg puts on a wire touching the
   selected item and on its own move ribbon (see the `selectedId` header
   comment there). One flag, three places it shows up, so a camera picked
   from the sidebar list lights up exactly as a direct tap on its pin does. */
.plot-line.is-selected {
  opacity: 1;
  stroke-width: calc(var(--linew, 2) * 1.8);
  filter: drop-shadow(0 0 3px var(--accent));
}
/* It carries is-selected only so the dimming rule leaves it visible; the
   moving pulse still keeps its own lighter width above the selected cable. */
.plot-line-flow.is-selected {
  stroke-width: calc(var(--linew, 2) * 1.4);
  filter: none;
}
.plot-move.is-selected .plot-move-path { opacity: 0.7; }
.plot-move.is-selected .plot-move-arrow,
.plot-move.is-selected .plot-move-end { filter: drop-shadow(0 0 2px var(--accent)); }
.plot-item.is-spotlit .plot-ring { stroke: var(--warn-strong); stroke-width: 3.4; }
/* Mid-drag on someone else's screen: it is where their finger is, not where
   anything has been saved yet. */
/* Somebody else has hold of this one, or it is mid-playback.
   Was opacity alone, at .65 — which on a pin that is MOVING reads as a pin
   that is moving, not as one under someone else's finger. The whole point of
   a ghost is that it is not where the plan says the thing is, so it now says
   so: a dashed ring and a hollow disc, the same "provisional" language the
   unlisted ring uses, plus the fade it always had. */
/* Crowded gear gets a box drawn round it rather than being collapsed into one
   bubble — you can still see what is in there, which is the whole point of a
   plot. Quiet enough to read as a boundary and not as a stage outline: no
   fill, a hairline, and the label small and set into the corner. */
.plot-group-box {
  fill: none; stroke: var(--panel-border); stroke-width: 1.6;
  vector-effect: non-scaling-stroke;
}
/* Smaller than an item's own label, and out of the way — it names the box,
   which is the least important thing in it.
   Counter-scaled like everything else on this overlay: font-size here is in
   MAP units, so a flat 9px was 9px only at the one zoom where --inv is 1. On a
   phone at fit it came out 3.5 screen pixels — present, and unreadable. Same
   construction as .plot-map-scale text below, for the same reason. */
.plot-group-label {
  fill: var(--dim); font-size: calc(9px * var(--inv, 1)); font-weight: 700;
  paint-order: stroke; stroke: #0b0d11; stroke-width: calc(2.5px * var(--inv, 1));
}
/* Members draw smaller inside it. This is the room that hiding them used to
   buy, bought without hiding them — and it is the same shrink `.is-tight`
   applies, so a box inside a crowded plot does not stack two reductions.

   MULTIPLIED into the counter-scale, never substituted for it. `transform` is
   one property: a bare `scale(.66)` here does not shrink the item by a third,
   it REPLACES `.plot-item-scale`'s `scale(var(--inv))` outright and throws the
   counter-scale away. That reads as intended on a desktop, where --inv is
   about 1 anyway, and falls apart on a phone at fit, where --inv is about 2.5
   and every member of a group drew at a quarter of its proper size — pins and
   captions alike, about 3 screen pixels of text. */
.plot-item.in-group .plot-item-scale { transform: scale(calc(var(--inv, 1) * .66)); }
.plot-label.in-group .plot-item-scale { transform: scale(calc(var(--inv, 1) * .66)); }
/* A caption that found nowhere clear to sit — see plotLabelLayout in app.js.
   Hidden rather than absent so the node is still there to count and to come
   back on the next zoom; the pin itself is untouched. */
.plot-label.is-crowded { display: none; }

.plot-item.is-ghost { opacity: 0.62; }
.plot-item.is-ghost .plot-ring {
  stroke: var(--text); stroke-dasharray: 4 3; stroke-width: 2;
}
.plot-item.is-ghost .plot-disc { fill-opacity: .35; }
/* Except while YOU are the one holding it — your own pin is not a ghost to
   you, and renderOverlay already drops your own id from the ghost list; this
   only guards the frame where both could briefly be true. */
.plot-item.is-ghost.is-selected .plot-ring { stroke-dasharray: none; }
/* Playing its own path. NOT a ghost: it shares "not at its stored position"
   with one, which is why it used to share the fade, but this is the camera you
   are deliberately watching and dimming it is the opposite of what that
   moment wants. Solid, and ringed in the accent so it is easy to follow
   across a busy plot. */
.plot-item.is-playing .plot-ring { stroke: var(--accent); stroke-width: 2.6; }
/* On the map, on no kit list. A dashed ring, and deliberately NOT a flash:
   this marks the pins nobody has written down, which on a plot that has just
   been laid out is most of them, and a dozen things blinking at once on the
   document you read positions off is the fastest way to make people stop
   looking at the plot. A broken ring says "unfinished" at a glance, survives
   being screenshotted, and costs nothing to ignore while you are doing
   something else. Selection and spotlight both overrule it — those are about
   right now, this is about later. */
.plot-item.is-unlisted .plot-ring {
  stroke: var(--warn); stroke-dasharray: 3 3; stroke-width: 2.2;
}
.plot-item.is-unlisted.is-selected .plot-ring,
.plot-item.is-unlisted.is-spotlit .plot-ring { stroke-dasharray: none; }
/* A group's box, when something inside it is on the map and on no list. */
.plot-group.is-unlisted .plot-group-box {
  stroke: var(--warn); stroke-dasharray: 4 3;
}
.plot-group.is-unlisted .plot-group-label { fill: var(--warn); }
@media (prefers-reduced-motion: no-preference) {
  .plot-item.is-spotlit .plot-ring { animation: plot-pulse 1.4s ease-in-out infinite; }
}
@keyframes plot-pulse {
  0%, 100% { opacity: 1; }
  50% { opacity: 0.25; }
}

/* ---- the meeting layer: lasers and pings ----

   Inside .plot-pan and therefore inside the map's own box, which is what keeps
   a mark on the map rather than merely near it: the layer carries the same
   transform as the image and the items, so there is nothing to keep in step.
   Never a hit target — everything here is somebody else pointing, and a
   pointer is not a thing you can touch. --who is always a validated hex colour
   (plotSafeColor); nothing else is ever interpolated into these rules. */
.plot-fx { position: absolute; inset: 0; pointer-events: none; }
.plot-fx > div { position: absolute; inset: 0; }
.plot-fx .plot-ping { position: absolute; inset: 0; }
.plot-fx-svg { display: block; width: 100%; height: 100%; overflow: visible; }

.plot-laser-trail {
  stroke: var(--who, #8b93a5);
  stroke-width: calc(var(--linew, 2) * 1.6);
  stroke-linecap: round;
  fill: none;
}
/* Two circles, because a laser dot is mostly its own bloom: the wide soft one
   is what you can find on a big screen from the back of a room, the small hard
   one is what you actually aim with. */
.plot-laser-glow { fill: var(--who, #8b93a5); opacity: 0.22; }
.plot-laser-dot { fill: var(--who, #8b93a5); stroke: #0b0d11; stroke-width: 1.2; }

.plot-ping-ring circle {
  fill: none;
  stroke: var(--who, #8b93a5);
  stroke-width: 3;
}
/* Scaled about the ping's own point: transform-box makes the reference box the
   viewBox, so 0 0 is this group's local origin — which the translate on its
   parent has already put where the tap landed. Same trick, same reason, as
   .plot-item-scale above. */
.plot-ping-ring {
  transform-box: view-box;
  transform-origin: 0 0;
  transform: scale(2.4);
  opacity: 0;
}
@media (prefers-reduced-motion: no-preference) {
  .plot-ping-ring { animation: plot-ripple 1.1s cubic-bezier(0.2, 0.7, 0.3, 1) forwards; }
}
/* Reduced motion still gets a mark — a ping that does nothing at all on a
   phone set to still animations is a gesture that silently fails for the
   person most likely to be told "look, there". It holds, then goes. */
@media (prefers-reduced-motion: reduce) {
  .plot-ping-ring { transform: scale(1.6); opacity: 1; animation: plot-ping-hold 1.1s steps(1, end) forwards; }
}
@keyframes plot-ripple {
  0% { transform: scale(0.35); opacity: 0.95; }
  100% { transform: scale(2.6); opacity: 0; }
}
@keyframes plot-ping-hold {
  0%, 70% { opacity: 1; }
  100% { opacity: 0; }
}

/* ---- borrowing somebody's view ---- */

/* Top right, under the bar, rather than along the bottom edge with everything
   else. The bottom of this page is already four deep — legend, flash, palette,
   item panel — and the panel is full-width on a phone, so a chip down there is
   a chip behind a card exactly when somebody is deciding whether to follow. */
/* --plot-top-h is the bar's measured height, published by topBarHeight(), and
   it already includes the safe-area inset — so no env() here. The fallback is
   only for the first paint, before anything has measured. Deriving it rather
   than restating it: the constant this replaced happened to be right, but
   nothing kept it right if the bar's padding or the pill's font changed. */
.plot-follow {
  position: fixed; z-index: 4;
  right: 12px; top: calc(var(--plot-top-h, 57px) + 6px);
  display: flex; flex-direction: column; align-items: flex-end; gap: 6px;
}
.plot-follow[hidden] { display: none; }
.plot-follow-chip {
  display: inline-flex; align-items: center; gap: 6px;
  max-width: min(260px, calc(100vw - 24px));
  padding: 8px 12px; border-radius: 999px;
  background: rgba(26, 29, 36, 0.94);
  border: 1px solid var(--panel-border);
  color: var(--text); font-size: 12px; font-weight: 600;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
/* The presenter's own colour as a dot on the chip, so two of them are telling
   you apart by the same mark their laser uses. */
.plot-follow-chip::before {
  content: ""; flex: none;
  width: 8px; height: 8px; border-radius: 50%;
  background: var(--who, #8b93a5);
}
.plot-follow-chip.is-on { border-color: var(--accent); color: var(--accent); }

/* ---- panels over the map ---- */

.plot-note {
  position: fixed; z-index: 3;
  left: 50%; top: 50%; transform: translate(-50%, -50%);
  width: min(420px, calc(100vw - 32px));
  padding: 22px; text-align: center;
  background: var(--panel); border: 1px solid var(--panel-border); border-radius: 12px;
}
.plot-note h1 { margin-bottom: 8px; }
.plot-note .subtitle { margin-bottom: 0; }
.plot-note .primary { margin-top: 12px; }
.plot-note .plot-note-hint { margin-top: 8px; font-size: 12px; }

.plot-legend-slot { position: fixed; z-index: 3; left: 12px; bottom: calc(12px + env(safe-area-inset-bottom, 0px)); }

/* The event name (and, if set, the current named version and Setup's own
   note line), as plain text over the map's own top-left corner — shown only
   above the tools breakpoint (see the media query below) and never embedded,
   where the planner's own header already names the event. Hidden by default
   so mobile and embed need no override of their own. pointer-events:none —
   this is a label, not a control, and must never steal a tap meant for the
   map underneath it. */
.plot-map-info {
  display: none; position: fixed; z-index: 3; left: 24px;
  top: calc(24px + env(safe-area-inset-top, 0px));
  max-width: min(60vw, calc(100vw - var(--plot-tools-w, 0px) - 60px));
  color: #fff; pointer-events: none;
  text-shadow: 0 1px 8px rgba(0, 0, 0, .7), 0 0 3px rgba(0, 0, 0, .5);
}
.plot-map-info-name { font-size: 20px; font-weight: 700; line-height: 1.25; }
.plot-map-info-version { margin-top: 4px; font-size: 13px; font-weight: 600; opacity: .85; }
.plot-map-info-note { margin-top: 2px; font-size: 12px; opacity: .75; }
body.plot-display .plot-map-info { display: none !important; }
/* Fit, zoom and Play paths, floating over the map's own bottom-right corner
   rather than living in the top bar (phone) or the desktop sidebar's Plot
   controls section — reachable from wherever you're actually looking, the
   map itself. Each button carries its own tile background rather than
   relying on a bar behind the row, the way .plot-back does, because there is
   no bar here for a transparent .ghost button to read against. */
/* The layer picker's button, on the map's own top-right corner where every
   map app puts one. Same treatment as the view controls in the opposite
   corner, so the two read as one set of map tools rather than two. */
.plot-map-controls {
  position: fixed; z-index: 6; right: 12px;
  top: calc(64px + env(safe-area-inset-top, 0px));
  display: flex; gap: 6px;
}
body.plot-display .plot-map-controls { display: none; }
.plot-map-controls .plot-utility-btn {
  background: rgba(20, 23, 29, .92); border: 1px solid var(--panel-border);
  border-radius: 8px; color: var(--text); box-shadow: 0 4px 14px rgba(0, 0, 0, .3);
}
.plot-map-controls .plot-utility-btn.is-on { border-color: var(--accent); color: var(--accent); }

.plot-view-controls {
  position: fixed; z-index: 4; right: 12px;
  bottom: calc(12px + env(safe-area-inset-bottom, 0px));
  display: flex; gap: 6px;
}
/* Laser and Present sit on this row too, and every button on it is an icon —
   their spelled-out labels would be the odd ones out, exactly as "Play paths"
   would have been. */
.plot-view-controls .plot-utility-label { display: none; }
.plot-view-controls .plot-utility-btn {
  background: rgba(20, 23, 29, .92); border: 1px solid var(--panel-border);
  border-radius: 8px; color: var(--text); box-shadow: 0 4px 14px rgba(0, 0, 0, .3);
}
/* An <svg> has no intrinsic size worth trusting (a bare viewBox defaults to
   300x150), so it is pinned to the icon grid its own path coordinates were
   drawn against — anything else and place-items:center would be centering a
   box far bigger than the icon actually drawn inside it. */
.plot-view-controls .plot-utility-btn svg,
.plot-map-controls .plot-utility-btn svg { width: 16px; height: 16px; display: block; }
.plot-view-controls[hidden] { display: none; }
body.plot-display .plot-view-controls { display: none; }
/* The planner's own "Focus map" pill (.plot-focus-actions in the PARENT
   document, planner.html) floats over this same corner of the iframe when
   the Plot tab is embedded, so this row needs enough clearance to sit above
   it rather than under it. Two constants in two files, kept in step by eye —
   see this element's own comment above plot.html for why there is nothing to
   share them through. */
body.plot-embed .plot-view-controls { bottom: calc(64px + env(safe-area-inset-bottom, 0px)); }
/* Focus mode puts a SECOND button in that parent stack — "Exit focus" plus
   "Show/Hide tools" — so it grows from about 36px tall to about 80px, and
   this row ends up inside it. Parent content paints over an iframe no
   matter what either one's z-index says, so the row simply vanished. It
   sits above the taller stack instead, which is what "always visible" has
   to mean here. Measured, not guessed: with one button the stack runs
   18–54px up from the bottom, with two it runs 18–98px. */
body.plot-parent-focus .plot-view-controls { bottom: calc(108px + env(safe-area-inset-bottom, 0px)); }
/* Focus mode with the tools put away hides the column, so the corner controls
   have to come back out to the edge with it. They were still sitting 344px in
   — the width of a sidebar that is not on screen — which left them stranded in
   the middle of the map with nothing to their right but the parent's own
   buttons. With the tools OPEN the column is back and so is the offset. */
body.plot-parent-focus:not(.plot-focus-tools-open) .plot-view-controls,
body.plot-parent-focus:not(.plot-focus-tools-open) .plot-map-controls,
body.plot-parent-focus:not(.plot-focus-tools-open) .plot-versions-drop,
body.plot-parent-focus:not(.plot-focus-tools-open) .plot-layers { right: 12px; }
.plot-scale-key { display: inline-flex; align-items: center; gap: 7px; margin-top: 6px; padding: 6px 9px; border-radius: 8px; background: rgba(20,23,29,.92); border: 1px solid var(--panel-border); color: var(--text); font-size: 11px; font-weight: 700; }
.plot-scale-key span { width: 34px; border-top: 2px solid var(--accent); }
.plot-legend {
  display: flex; flex-wrap: wrap; gap: 6px 14px;
  padding: 8px 11px; border-radius: 10px;
  background: rgba(26, 29, 36, 0.92); border: 1px solid var(--panel-border);
  font-size: 11px; color: var(--dim);
}
.plot-legend-item { display: inline-flex; align-items: center; gap: 6px; }
/* Ten wire types in one flat row is a paragraph. Grouping them under the
   family name is what keeps the key readable — and the family is the thing
   the colour actually means, so the grouping is not just tidying. */
.plot-legend-group { display: inline-flex; align-items: center; flex-wrap: wrap; gap: 4px 10px; }
.plot-legend-family { color: var(--text); font-weight: 600; }
.plot-legend-dash { width: 18px; height: 0; border-top: 2px solid var(--sig); }
.plot-legend-dash.is-wireless,
.plot-legend-dash.type-comms,
.plot-legend-dash.type-custom { border-top-style: dashed; }
.plot-legend-dash.type-timecode { border-top-style: dotted; }
/* An outline's own swatch: a small closed box rather than a dash, because a
   closed figure is the whole difference between it and every line above.
   --shape is the kind's own colour, from PLOT_SHAPE_KINDS and nowhere else. */
.plot-legend-shape {
  width: 16px; height: 10px;
  border: 1.5px solid var(--shape, var(--dim));
}
/* The same border styles the outlines themselves carry, so the key is drawn
   in the map's own language rather than in a second one. */
.plot-legend-shape.shape-barrier { border-style: dashed; }
.plot-legend-shape.shape-zone { border-style: dotted; }

/* The move's own swatch: thicker than a signal dash and pointing, which is
   the whole difference between the two kinds of line on the map. */
.plot-legend-move { position: relative; width: 18px; height: 0; border-top: 3px solid var(--dim); opacity: 0.8; }
.plot-legend-move::after {
  content: ""; position: absolute; right: -5px; top: -3.5px;
  border-left: 5px solid var(--dim);
  border-top: 3.5px solid transparent; border-bottom: 3.5px solid transparent;
}
/* The coverage list: every wireless-flagged camera with nothing wired to it
   yet. Same amber as the pin badge (plot-wireless-gap-badge), same reasoning
   — a checklist item, not a fault, so only the small heading and the dot
   carry the colour; the camera's own name stays the legend's ordinary --dim,
   same as every other entry. */
.plot-legend-family.is-warn { color: var(--warn); }
.plot-legend-gap-dot { width: 8px; height: 8px; border-radius: 50%; border: 1.5px dashed var(--warn); }

/* ---- layers panel ----
 * A corner panel on a wide screen, not a modal like the passcode prompt or
 * the versions box: the whole point is watching the map change as a toggle is
 * touched, so nothing here dims or blocks the rest of the screen. On a phone
 * (see the narrow-screen override below) a corner card of this height would
 * land on the middle of the map rather than beside it — the map is centred
 * in whatever room `fit()` leaves around it, not pinned to an edge — so
 * narrow screens get the same bottom-docked sheet `.plot-sheet` already
 * uses instead. */
/* The versions list, hanging under its own button on the map's corner — the
   same box the modal uses, just parked somewhere else, so there is one list
   and one set of actions rather than two to keep in step. Sized and placed
   like the layers card beside it; below the breakpoint this host is never
   used and the modal takes over. */
.plot-versions-drop {
  position: fixed; z-index: 5;
  top: calc(104px + env(safe-area-inset-top, 0px));
  right: 12px;
  width: min(280px, calc(100vw - 24px));
  max-height: min(70vh, 520px); overflow-y: auto;
  padding: 12px; border-radius: 12px;
  background: rgba(20, 23, 29, 0.96);
  border: 1px solid var(--panel-border);
  box-shadow: 0 10px 30px rgba(0, 0, 0, 0.45);
}
.plot-versions-drop[hidden] { display: none; }

/* Hangs under its own button rather than starting level with it — the button
   is at 64px and is 34px tall, so this clears it by 6. */
.plot-layers {
  position: fixed; z-index: 5;
  top: calc(104px + env(safe-area-inset-top, 0px));
  right: 12px;
  width: min(230px, calc(100vw - 24px));
  max-height: min(70vh, 520px);
  overflow-y: auto;
  display: flex; flex-direction: column; gap: 10px;
  padding: 12px;
  background: rgba(20, 23, 29, 0.96);
  border: 1px solid var(--panel-border); border-radius: 12px;
  box-shadow: 0 10px 30px rgba(0, 0, 0, 0.45);
}
.plot-layers-head { display: flex; align-items: center; justify-content: space-between; gap: 10px; }
.plot-layers-head h2 { margin: 0; font-size: 14px; }
.plot-layers-body { display: flex; flex-direction: column; gap: 6px; }
.plot-layer-checks { display: grid; grid-template-columns: 1fr; gap: 5px; }
.plot-layer-check { width: 100%; min-width: 0; align-items: center; padding: 7px 8px; border: 1px solid var(--panel-border); border-radius: 9px; background: var(--panel); transition: opacity .15s, filter .15s; }
.plot-layer-check input { position: absolute; opacity: 0; pointer-events: none; }
.plot-layer-check i { width: 28px; height: 28px; flex: 0 0 28px; display: grid; place-items: center; border-radius: 7px; background: rgba(79,141,253,.14); color: var(--accent); font-style: normal; }
.plot-layer-check i svg { width: 22px; height: 22px; fill: none; stroke: currentColor; stroke-width: 1.7; stroke-linecap: round; stroke-linejoin: round; }
.plot-layer-check:has(input:not(:checked)) { opacity: .42; filter: grayscale(1); }
.plot-layer-check > span { display: flex; flex-direction: column; gap: 1px; }
.plot-layer-check b { color: var(--text); font-size: 12px; }
.plot-layer-check small { color: var(--dim); font-size: 10px; line-height: 1.25; }
.plot-layer-kinds { display: grid; grid-template-columns: 1fr 1fr; gap: 5px; }
.plot-layer-kinds .plot-layer-check { display: grid; grid-template-columns: 28px minmax(0,1fr); }
.plot-layers-sub {
  font-size: 10px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.04em;
  color: var(--dim); margin-top: 4px;
}
.plot-dim-row {
  display: flex; flex-direction: column; gap: 6px;
  padding-top: 8px; border-top: 1px solid var(--panel-border);
}
.plot-dim-row label { font-size: 12px; color: var(--dim); display: flex; justify-content: space-between; }
/* Says out loud what the mono toggle under it already implies: this darkens
   the map on THIS screen and nowhere else. It used to be written into the
   plan, so it is worth being explicit that it no longer is. Quieter than the
   setting's own name, on the same line — there is no room for a second one. */
.plot-dim-row label small { font-size: 11px; opacity: .7; font-weight: 400; margin-left: 4px; }
/* Built as a .plot-layer-check because it IS one — the same row of icon,
   name and one line of explanation the layer toggles use. It only needs
   telling not to take the space-between the dim slider's own <label> above
   sets on every label in this row. */
.plot-dim-row label.plot-mono-check { justify-content: flex-start; margin-top: 2px; }
/* Greyed at the source — the <img> itself — so nothing drawn ON the map is
   touched. The pins, cables and paths keep their colours, which is the whole
   point: on this plot colour means which kind of run and which camera, and
   greying that out would remove the thing the quiet background is for. */
body.plot-mono .plot-map { filter: grayscale(1); }
.plot-dim-row input[type="range"] { width: 100%; accent-color: var(--accent); }

/* Same corner-panel shape as .plot-layers just above, sharing its box model
   line for line — see that block's own comment for why a phone gets a
   bottom-docked override instead (the narrow-screen block below carries the
   same override for this panel). They differ only in what goes in the body:
   a scrolling list of buttons rather than checkboxes. */
.plot-cameras {
  position: fixed; z-index: 5;
  top: calc(64px + env(safe-area-inset-top, 0px));
  right: 12px;
  width: min(230px, calc(100vw - 24px));
  max-height: min(70vh, 520px);
  overflow-y: auto;
  display: flex; flex-direction: column; gap: 8px;
  padding: 12px;
  background: rgba(20, 23, 29, 0.96);
  border: 1px solid var(--panel-border); border-radius: 12px;
  box-shadow: 0 10px 30px rgba(0, 0, 0, 0.45);
}
.plot-cameras-head { display: flex; align-items: center; justify-content: space-between; gap: 10px; }
.plot-cameras-head h2 { margin: 0; font-size: 14px; }
.plot-cameras-body { display: flex; flex-direction: column; gap: 4px; }
.plot-cameras-row {
  display: flex; align-items: center; gap: 9px; width: 100%; text-align: left;
  padding: 8px 10px; border-radius: 8px;
  border: 1px solid var(--panel-border); background: var(--panel-2);
  color: var(--text); font-size: 13px; font-weight: 600;
  cursor: pointer;
}
.plot-cameras-row:hover { border-color: var(--accent); }
/* Draggable into order while editing — grab anywhere on the row, since the
   row IS the handle here (the Gear tab has a grip because its rows are full
   of fields; these are not). */
body.plot-editing .plot-cameras-row { cursor: grab; touch-action: none; }
body.plot-editing .plot-cameras-row.is-dragging {
  cursor: grabbing; border-color: var(--accent); opacity: .85;
}
/* The camera's own plot colour, so the row and its pin agree without reading
   either. Falls back to the ordinary accent when nothing has been marked. */
.plot-cameras-dot {
  flex: none; width: 9px; height: 9px; border-radius: 50%;
  background: var(--dot, var(--accent));
}
.plot-cameras-text { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 1px; }
.plot-cameras-name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.plot-cameras-sub {
  font-size: 11px; font-weight: 400; color: var(--dim);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.plot-cameras-tag {
  flex: none; padding: 1px 6px; border-radius: 999px; font-size: 10px;
  background: var(--panel-border); color: var(--dim); font-weight: 700;
}
/* The way from reading a camera to editing it, on the card itself. */
.plot-card-edit { flex: none; white-space: nowrap; }

/* The row carries a button when it is offering an undo, so it lays out as a
   row rather than as a line of text. */
.plot-flash { display: flex; align-items: center; gap: 10px; }
.plot-flash-undo {
  flex: none; padding: 3px 10px; border-radius: 7px; cursor: pointer;
  background: none; border: 1px solid var(--accent); color: var(--accent);
  font-size: 12px; font-weight: 700;
}
.plot-flash-undo:hover { background: var(--accent); color: #fff; }

.plot-flash {
  position: fixed; z-index: 5;
  left: 50%; transform: translateX(-50%);
  bottom: calc(16px + env(safe-area-inset-bottom, 0px));
  max-width: calc(100vw - 32px);
  padding: 10px 14px; border-radius: 10px;
  background: var(--panel-3); border: 1px solid var(--panel-border);
  font-size: 13px;
  /* It sits in the middle of the map and it is only ever telling you
     something. "Tap the map to place the camera" catching the tap it just
     asked for is the kind of bug you blame on your own aim. */
  pointer-events: none;
}

/* ---- editing ----
   Everything in this block is behind the passcode and off screen until then,
   which is why none of it costs the crew-phone case anything. */

/* The three edit panels are all `display: flex` when shown, and a class
   selector beats the browser's own `[hidden] { display: none }` — without this
   the hidden attribute does nothing to them and the passcode prompt greets
   everybody who opens the page. */
.plot-palette[hidden], .plot-sheet[hidden], .plot-ask[hidden],
.plot-wires[hidden], .plot-pal-ghost[hidden], .plot-versions[hidden],
.plot-layers[hidden], .plot-cameras[hidden],
.plot-tools[hidden], .plot-tools-sec[hidden], .plot-pal-group[hidden], .plot-wire-delete[hidden] { display: none; }

/* No position of its own any more: it is a child of .plot-view-controls and
   rides that row's corner. What went with it was a whole parallel set of
   hand-measured heights — one for embedded, one for focus mode — that existed
   only to stack this above a row it is now simply part of. Two constants kept
   in step by eye became none. */
.plot-wire-delete {
  display: inline-flex; align-items: center; gap: 7px;
  padding: 10px 13px; border-radius: 10px; background: rgba(20,23,29,.96);
  box-shadow: 0 6px 20px rgba(0,0,0,.35);
}

/* Armed and waiting for the next tap on the map. Crosshair on a desktop; on a
   phone the palette button lighting up is the only cue there can be. */
body.plot-placing .plot-stage { cursor: crosshair; }
body.plot-editing .plot-item { cursor: grab; }

.plot-handle { pointer-events: auto; cursor: grab; }
.plot-handle line { stroke: var(--accent); stroke-width: 2.4; }
.plot-handle-dot { fill: var(--accent); stroke: #0b0d11; stroke-width: 1.6; }

/* Drawing a move arms the map for taps, exactly as placing an item does, so it
   gets the same cursor — and it has to beat the grab cursor the rule above puts
   on every item, because while drawing nothing on the map is grabbable. */
body.plot-drawing .plot-stage,
body.plot-drawing.plot-editing .plot-item,
body.plot-shaping .plot-stage,
body.plot-shaping.plot-editing .plot-item,
/* Setting the scale is a drag across the map like the two above, so it says
   so the same way — and over items too, since the thing of known length is
   as likely to have gear standing on it as not. */
body.plot-scaling .plot-stage,
body.plot-scaling.plot-editing .plot-item { cursor: crosshair; }

/* SETTING THE SCALE CLEARS THE MAP. The line being drawn is measured against
   something ON the map — a stage edge, a road, a barrier run — and the whole
   plot standing on top of it is in the way of seeing the thing you are
   aiming at. So while the tool is armed the pins, wires, labels and outlines
   step aside and the darkening comes off, leaving the map as it was
   uploaded. Everything comes straight back when the tool disarms; nothing
   here changes any of it, only what is painted.

   The scrim carries an inline opacity from setDim, so this needs
   !important to beat it — the one place in this file where that is the
   honest tool rather than a shortcut. The scale line itself lives outside
   .plot-overlay, so it stays visible while everything else goes. */
body.plot-scaling .plot-overlay { opacity: 0; pointer-events: none; }
body.plot-scaling .plot-scrim { opacity: 0 !important; }
body.plot-scaling .plot-fx { opacity: 0; }
.plot-overlay, .plot-scrim, .plot-fx { transition: opacity .16s ease; }

/* A move's points, while its camera is selected. Hollow, and smaller than the
   rotate handle beside them: there can be twelve of these at once, and twelve
   filled accent discs would read as twelve more items rather than as grips on
   one line. */
.plot-move-handle { pointer-events: auto; cursor: grab; }
.plot-move-handle-dot { fill: var(--panel); stroke: var(--accent); stroke-width: 2.4; }
/* Two cases, one rule: while the move is being drawn the map's whole job is to
   catch the next tap, and while somebody else is redrawing this move the grips
   are following their draft rather than the points a drag here would edit.

   It has to name the grip, not just the layer. `pointer-events: none` on a
   parent does not stop a descendant that sets it back to `auto` from being hit,
   and the rule above sets exactly that on every grip — so a layer-only version
   of this looks right in the stylesheet and blocks nothing at all. Confirmed
   with elementFromPoint rather than assumed. */
.plot-move-handles.is-inert,
.plot-move-handles.is-inert .plot-move-handle { pointer-events: none; }
.plot-move-tools { display: flex; flex-wrap: wrap; gap: 6px; }

/* A wire's own waypoints — "cable pins" — the move handle's own trick in the
   wire's own colour (--sig, set inline per handle, the same custom property
   a wire-list row already carries) rather than the move's fixed accent: two
   things you can drag on the same map need to read as two different kinds of
   grip. addWireHandles only ever builds this layer while editing and with
   nothing else armed, so unlike the move handles is-inert is never a
   whole-layer question here — it is set per HANDLE instead, for the one wire
   (if any) somebody else is currently mid-reshape on; see addWireHandles'
   own comment (Important 3, round 3's review). */
.plot-wire-handle { pointer-events: auto; cursor: grab; }
.plot-wire-handle.is-inert { pointer-events: none; }
.plot-wire-handle-dot { fill: var(--panel); stroke: var(--sig, var(--accent)); stroke-width: 2.2; }
.plot-wire-handle.is-selected .plot-wire-handle-dot {
  fill: var(--sig, var(--accent)); stroke: #fff; stroke-width: 2.8;
  filter: drop-shadow(0 0 4px var(--sig, var(--accent)));
}
.plot-move-tools button { padding: 7px 10px; font-size: 12px; font-weight: 600; }

/* The palette owns the bottom edge in edit mode, so the two things that
   already live there move up out of its way rather than under it — and up
   again by the wire picker's own measured height when that is open too.
   --plot-wires-h is published by wiresHeight() and is 0px whenever the picker
   is not on screen, so this is the old rule plus nothing in the common case. */
body.plot-editing .plot-legend-slot { bottom: calc(84px + var(--plot-wires-h, 0px) + env(safe-area-inset-bottom, 0px)); }
body.plot-editing .plot-flash { bottom: calc(88px + var(--plot-wires-h, 0px) + env(safe-area-inset-bottom, 0px)); }

.plot-palette {
  position: fixed; z-index: 4;
  left: 0; right: 0; bottom: 0;
  display: flex; align-items: stretch; gap: 8px;
  padding: 8px 10px calc(8px + env(safe-area-inset-bottom, 0px));
  background: rgba(20, 23, 29, 0.96);
  border-top: 1px solid var(--panel-border);
}
/* Six kinds do not fit across a 375px phone, and shrinking them to fit makes
   six targets nobody can hit. They scroll instead. */
.plot-pal-kinds {
  flex: 1; min-width: 0;
  display: flex; gap: 6px;
  overflow-x: auto; overscroll-behavior-x: contain;
  scrollbar-width: none;
}
.plot-pal-kinds::-webkit-scrollbar { display: none; }
.plot-pal-tools { flex: none; display: flex; align-items: center; gap: 8px; }
/* The tool column's handles on parts of this row (see plot.html). `contents`
   rather than a real box on purpose: down here the buttons must go on being
   flex items of .plot-pal-tools itself, laid out and spaced exactly as they
   were when they were loose children, so that grouping them for the column's
   benefit costs the phone nothing at all. */
.plot-pal-group { display: contents; }
/* The presenter toggles live in the tool row rather than with the kinds,
   because they are not things you place — they are things you are doing, and
   they stay lit while you are doing them. */
/* The second selector is the same two buttons after the tool column has moved
   them into the top bar — the same treatment following them there, rather than
   a second declaration of the same colours beside a different selector. */
.plot-pal-tools .ghost.is-on,
.plot-top .plot-pal-group .ghost.is-on { background: var(--accent); border-color: var(--accent); color: #fff; }
#plot-laser.is-on, #plot-present.is-on { background: #dc2626; border-color: #ef4444; color: #fff; box-shadow: 0 0 0 3px rgba(239, 68, 68, 0.18); }
body.plot-laser-on .plot-stage { cursor: crosshair; }
/* Route, which lives in the picker's own head rather than in the tool row, so
   neither selector above ever reached it — the class was being toggled onto a
   button nothing styled, and an armed mode that changes what every tap on the
   map means looked exactly like an unarmed one. Scoped to .plot-wires rather
   than to the head, because the picker is one node that MOVES: above the
   breakpoint it is carried whole into the tool column (see TOOL_MOVES), and a
   selector describing only one of the two layouts is the same bug again. */
.plot-wires .ghost.is-on { background: var(--accent); border-color: var(--accent); color: #fff; }

.plot-pal-btn {
  flex: none;
  display: flex; flex-direction: column; align-items: center; gap: 2px;
  width: 58px; padding: 6px 2px;
  font-size: 10px; font-weight: 600; color: var(--dim);
  background: transparent; border-color: transparent;
}
/* A vertical drag off a kind is how it gets carried onto the map, so the
   browser must not claim it as a scroll: only the sideways pan through the
   strip is native. Without this, a touch drag towards the map is answered with
   a pointercancel and the gesture ends before the ghost appears. */
.plot-pal-btn { touch-action: pan-x; }
@media (min-width: 900px) { .plot-tools .plot-pal-btn { touch-action: none; } }
.plot-pal-btn svg { width: 26px; height: 26px; display: block; }
.plot-pal-btn .plot-icon { stroke-width: 1.6; stroke: var(--dim); }
.plot-pal-btn:hover { border-color: var(--panel-border); }
/* Armed is a state, not a hover: the very next touch anywhere on the map
   commits to it, so it has to be unmistakable from arm's length. */
.plot-pal-btn.is-on {
  background: rgba(79, 141, 253, 0.16);
  border-color: var(--accent); color: var(--accent);
}
.plot-pal-btn.is-on .plot-icon { stroke: var(--accent); }
.plot-pal-btn:disabled { opacity: 0.35; }

/* The kind on its way to the map, under the pointer. Pointer-events-free
   because the drop asks elementFromPoint what is actually beneath the finger,
   and a ghost that answered that question with itself would drop every item at
   the bottom of the screen. Centred on the pointer by a negative margin rather
   than by a translate, so the transform stays free for the position. */
.plot-pal-ghost {
  position: fixed; z-index: 7; left: 0; top: 0;
  margin: -22px 0 0 -22px;
  width: 44px; height: 44px;
  display: flex; align-items: center; justify-content: center;
  border-radius: 50%;
  background: rgba(20, 23, 29, 0.92);
  border: 1px solid var(--accent);
  box-shadow: 0 6px 18px rgba(0, 0, 0, 0.5);
  pointer-events: none;
}
.plot-pal-ghost svg { width: 26px; height: 26px; display: block; }
.plot-pal-ghost .plot-icon {
  fill: none; stroke: var(--accent); stroke-width: 1.6;
  stroke-linecap: round; stroke-linejoin: round;
}
.plot-wire-ghost-line { width: 28px; border-top: 3px solid var(--wire, var(--accent)); box-shadow: 0 0 8px var(--wire, var(--accent)); }

/* ---- wires ---- */

/* The picker sits above the palette rather than inside it: ten types will not
   fit in the kinds row on a phone, and unlike a kind a wire type stays armed
   while several cables are run, so the panel has to stay on screen with it.
   --plot-pal-h is the palette's own measured height (wiresHeight), because the
   bar grows with the safe-area inset and a constant restating it would be one
   more number to keep in step by hand. */
.plot-wires {
  position: fixed; z-index: 4;
  left: 0; right: 0; bottom: var(--plot-pal-h, 68px);
  display: flex; flex-direction: column; gap: 8px;
  padding: 8px 10px;
  max-height: 34vh; overflow-y: auto;
  background: rgba(20, 23, 29, 0.96);
  border-top: 1px solid var(--panel-border);
}
.plot-wires-head { display: flex; align-items: center; gap: 10px; }
.plot-wires-hint {
  flex: 1; min-width: 0; font-size: 12px; color: var(--dim);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.plot-wires-body { display: flex; flex-wrap: wrap; gap: 8px 16px; }
.plot-wire-group { display: flex; align-items: center; flex-wrap: wrap; gap: 5px; }
.plot-wire-family {
  font-size: 10px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.04em;
  color: var(--dim); margin-right: 2px;
}
/* The chip carries the same dash the legend and the map draw that type with,
   so what is being armed looks like what will land. */
.plot-wire-chip {
  display: inline-flex; align-items: center; gap: 6px;
  padding: 7px 10px; font-size: 12px; font-weight: 600; color: var(--dim);
}
.plot-wire-chip { touch-action: none; cursor: grab; }
/* Armed is a state, not a hover: the next press on the map commits to it, so
   it has to be unmistakable from arm's length — the palette's own rule. */
.plot-wire-chip.is-on {
  background: rgba(79, 141, 253, 0.16);
  border-color: var(--accent); color: var(--accent);
}

/* A press on a thing starts a cable rather than dragging it, so the cursor over
   an item has to say so — and it has to beat the grab cursor above, exactly as
   the drawing cursor does. */
body.plot-wiring .plot-item { cursor: crosshair; }

/* The wire being dragged. Broken and slightly faded while it is still only a
   proposal; solid and full strength the moment it locks onto something, so
   "it will lock to it" is legible from the line as well as from the ring. */
.plot-wire-draft { position: absolute; inset: 0; pointer-events: none; }
.plot-wire-line {
  stroke-width: calc(var(--linew, 2) * 1.6);
  stroke-linecap: round;
  stroke-dasharray: calc(var(--linew, 2) * 4) calc(var(--linew, 2) * 3);
  opacity: 0.7;
}
.plot-wire-line.is-locked { stroke-dasharray: none; opacity: 1; }

/* The scale's reference line while it is being dragged out. Solid, not
   dashed like a wire proposal — it isn't going to "lock onto" anything, it is
   simply the line as drawn — and in the accent colour so it reads as tooling
   rather than as any wire family's own colour. */
.plot-scale-draft { position: absolute; inset: 0; pointer-events: none; }
.plot-scale-line {
  stroke: var(--accent);
  stroke-width: calc(var(--linew, 2) * 1.6);
  stroke-linecap: round;
}
.plot-scale-line.is-saved { stroke-dasharray: 8 5; opacity: .9; }
/* Wide, invisible, and the only pressable thing in an otherwise inert layer —
   the drawn line is a couple of pixels and nobody lands on that. */
.plot-scale-hit {
  stroke: transparent; stroke-width: calc(var(--linew, 2) * 9);
  stroke-linecap: round; pointer-events: stroke; cursor: pointer;
}
.plot-scale-line.is-selected {
  stroke: var(--warn-strong); stroke-dasharray: none;
  stroke-width: calc(var(--linew, 2) * 2.6); opacity: 1;
}
.plot-map-scale { fill: var(--text); stroke: var(--text); stroke-width: calc(2px * var(--inv)); }
.plot-map-scale text { stroke: rgba(15,17,21,.95); stroke-width: calc(3px * var(--inv)); paint-order: stroke; font-size: calc(12px * var(--inv)); font-weight: 700; }
.plot-scale-label { fill: var(--accent); font-size: 15px; font-weight: 800; paint-order: stroke; stroke: #0b0d11; stroke-width: 4px; }
.plot-play-btn { display: inline-grid; place-items: center; width: 34px; min-width: 34px; padding-inline: 0; }

/* The outline being tapped out, corner by corner. An open line and a dot per
   corner — no fill, because there is no inside until it closes — and the first
   corner drawn as the ring it is, since tapping it again is what closes the
   shape and nothing else on screen says so. */
.plot-shape-draft { position: absolute; inset: 0; pointer-events: none; }
.plot-shape-draft-path {
  stroke-width: calc(var(--linew, 2) * 1.6);
  stroke-linecap: round;
  stroke-linejoin: round;
  stroke-dasharray: calc(var(--linew, 2) * 4) calc(var(--linew, 2) * 3);
  opacity: 0.85;
}
.plot-shape-draft-dot { stroke: #0b0d11; stroke-width: 1.4; }
.plot-shape-draft-dot.is-first { stroke: var(--text); stroke-width: 2.4; }

/* A corner of a cable run being tapped out, drawn in the armed wire's own
   colour on the same dashed proposal line a dragged cable uses. Outlined like
   an outline's corner above and for the same reason — a filled dot in a wire
   family's colour, over a dark site map, is hard to pick out from the line it
   sits on. There is no is-first here: a run's first point is the pin it leaves,
   which is already ringed, not a corner of its own. */
.plot-route-dot { stroke: #0b0d11; stroke-width: 1.4; }

/* --wire is the armed type's own colour, published onto .plot-pan, so the ring
   round the thing about to be wired is the colour of the cable that would land
   on it. It falls back to the accent, which is what a source ring uses before
   any type has been chosen. */
/* The runs belonging to the selected camera. Thickened and brought up rather
   than recoloured: the colour of a cable on this map already means which KIND
   of run it is, and overriding that to say "selected" would trade a fact for
   a state. Everything else steps back instead. */
.plot-overlay:has(.is-run-lit) .plot-line:not(.is-run-lit),
.plot-overlay:has(.is-run-lit) .plot-line-flow:not(.is-run-lit) { opacity: .25; }
.plot-line.is-run-lit { stroke-width: 4.2; }

.plot-item.is-wire-source .plot-ring { stroke: var(--wire, var(--accent)); stroke-width: 2.6; }
.plot-item.is-wire-target .plot-ring { stroke: var(--wire, var(--accent)); stroke-width: 4.2; }
.plot-item.is-wire-target .plot-disc { stroke: var(--wire, var(--accent)); stroke-width: 2.4; }

/* One row per wire in the item's panel, each with its own way off. */
.plot-wire-list { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 4px; }
.plot-wire-row {
  display: flex; align-items: center; flex-wrap: wrap; gap: 8px;
  padding: 5px 6px 5px 10px; font-size: 13px; color: var(--text);
  background: var(--panel-2);
  border: 1px solid var(--panel-border); border-radius: 8px;
}
.plot-wire-name { flex: none; font-weight: 600; }
/* The Tx/Rx letter riding alongside a wireless wire's own name — dimmer and
   unbolded, since the type name it sits next to already carries the weight;
   this is a cross-reference to the pin, not a second heading. */
.plot-wire-tag { font-weight: 400; color: var(--dim); }
.plot-wire-to { flex: 1; min-width: 0; color: var(--dim); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* Same amber as the pin badge and the legend's coverage list, same reasoning:
   a checklist nudge sitting right where the Wireless toggle above it was
   switched on, not an error banner. */
.plot-wire-gap-hint { font-size: 12px; color: var(--warn); margin-top: 4px; }
/* Only present once state.plot.scale exists — wireLengthLabel answers "" with
   no scale set, and the template leaves this out entirely rather than print
   an empty span. Tabular figures so a column of these lines up. */
.plot-wire-length { flex: none; color: var(--dim); font-variant-numeric: tabular-nums; }
/* A real target rather than a 12px glyph: this is the one control on the panel
   that throws something away, and it is aimed at with a thumb. */
.plot-wire-x {
  flex: none; width: 30px; height: 30px; padding: 0;
  font-size: 17px; line-height: 1; color: var(--dim);
}
.plot-wire-x:hover { color: var(--bad); border-color: var(--bad); }

/* The wire's own waypoints, one small button per pin. flex-basis: 100% forces
   this onto its own line below the row above, rather than fighting the name
   and target for the same one — a row with several points would otherwise
   wrap in the middle of a sentence. */
.plot-wire-points { flex: 0 0 100%; display: flex; flex-wrap: wrap; gap: 4px; padding-left: 22px; }
.plot-wire-point-x {
  flex: none; padding: 3px 7px; font-size: 11px; color: var(--dim);
  background: var(--panel); border: 1px solid var(--panel-border); border-radius: 6px;
}
.plot-wire-point-x:hover { color: var(--bad); border-color: var(--bad); }

/* ---- the item panel ---- */

.plot-sheet {
  position: fixed; z-index: 6;
  left: 0; right: 0;
  /* Clear of the palette, and clear of the wire picker as well when that is
     open — the panel is above both in z-order, so without this it would simply
     cover the chips you are picking a cable from. */
  bottom: calc(74px + var(--plot-wires-h, 0px) + env(safe-area-inset-bottom, 0px));
  max-height: 58vh;
  display: flex; flex-direction: column;
  background: var(--panel);
  border-top: 1px solid var(--panel-border);
  border-radius: 14px 14px 0 0;
  box-shadow: 0 -12px 30px rgba(0, 0, 0, 0.45);
}
/* Read-only, so there is no palette underneath it to clear. */
.plot-sheet.is-card { bottom: calc(12px + env(safe-area-inset-bottom, 0px)); left: 10px; right: 10px; border-radius: 14px; border: 1px solid var(--panel-border); }
/* On a phone the panel and the picker are both bottom sheets, and 58vh plus
   34vh plus the palette is more than the screen. The panel is the one that
   gives, because the picker is what is being reached for. */
body.plot-wires-open .plot-sheet { max-height: 40vh; }

.plot-sheet-head {
  flex: none;
  display: flex; align-items: flex-start; justify-content: space-between; gap: 10px;
  padding: 12px 14px; border-bottom: 1px solid var(--panel-border);
}
.plot-sheet-title { font-size: 15px; font-weight: 600; }
.plot-sheet-kind { font-size: 12px; color: var(--dim); }
.plot-sheet-body {
  overflow-y: auto; -webkit-overflow-scrolling: touch;
  padding: 12px 14px 16px;
  display: flex; flex-direction: column; gap: 12px;
}
/* `label.field` already exists for the schedule's forms; the support and crew
   groups are label-less <div>s (a <label> wrapping six buttons or nine
   checkboxes doesn't mean anything), so the styling is widened to both. */
.plot-sheet-body .field { display: flex; flex-direction: column; gap: 4px; font-size: 12px; color: var(--dim); }
.plot-sheet-body .field > span { text-transform: uppercase; letter-spacing: 0.04em; }
.plot-sheet-body input[type="text"], .plot-sheet-body select, .plot-sheet-body textarea { width: 100%; }
.plot-sheet-body textarea { min-height: 64px; }
.plot-sheet-body select:disabled { opacity: 0.45; cursor: not-allowed; }
.plot-sheet-foot { display: flex; justify-content: flex-end; padding-top: 4px; }
.plot-pair { display: flex; gap: 10px; }
.plot-pair > * { flex: 1; min-width: 0; }
.plot-pair select { width: 100%; }

/* Six supports, wrapping — a segmented control rather than another dropdown
   because it is the field most often changed while standing next to the
   camera it describes. */
.plot-seg { display: flex; flex-wrap: wrap; gap: 5px; }
.plot-seg button { padding: 7px 10px; font-size: 12px; font-weight: 600; color: var(--dim); }
.plot-seg button.is-on { background: var(--accent); border-color: var(--accent); color: #fff; }

/* Ten swatches plus a text "None" — a grid of circles reads faster than
   another row of labelled buttons would for a colour choice, and keeping
   None as text (rather than a slashed circle) means it never gets mistaken
   for an eleventh colour. */
.plot-colours { display: flex; flex-wrap: wrap; align-items: center; gap: 6px; }
.plot-colour-swatch {
  width: 26px; height: 26px; padding: 0; border-radius: 50%;
  border: 2px solid var(--panel-border); background: var(--swatch);
}
.plot-colour-swatch.is-on { border-color: #fff; box-shadow: 0 0 0 2px var(--accent); }
.plot-colour-none {
  height: 26px; padding: 0 10px; border-radius: 13px;
  font-size: 11px; font-weight: 600; color: var(--dim);
}
.plot-colour-none.is-on { background: var(--accent); border-color: var(--accent); color: #fff; }

.plot-checks { display: flex; flex-wrap: wrap; gap: 5px; }
.plot-check {
  display: inline-flex; align-items: center; gap: 6px;
  padding: 7px 10px; border-radius: 8px;
  border: 1px solid var(--panel-border); background: var(--panel-2);
  font-size: 13px; cursor: pointer;
}
.plot-check input { margin: 0; accent-color: var(--accent); }
.plot-none { font-size: 12px; color: var(--dim); }

.plot-card-rows { display: grid; grid-template-columns: auto 1fr; gap: 4px 12px; margin: 0; }
.plot-card-rows dt { font-size: 11px; text-transform: uppercase; letter-spacing: 0.04em; color: var(--dim); padding-top: 2px; }
.plot-card-rows dd { margin: 0; font-size: 14px; }
.plot-card-notes { margin: 0; font-size: 13px; color: var(--dim); line-height: 1.5; white-space: pre-wrap; }
/* Said on the card because this is where somebody is looking when they ask
   what a camera needs — read off the plot's own wiring, never stored. */
.plot-card-warn {
  margin: 0; padding: 7px 9px; border-radius: 8px; font-size: 12px; line-height: 1.45;
  background: color-mix(in srgb, var(--warn) 16%, transparent);
  border: 1px solid color-mix(in srgb, var(--warn) 45%, transparent); color: var(--warn);
}
/* Where this one stands, cut out of the site plan itself. The image is
   oversized and slid under a fixed window — see cardThumbHtml — so the crop
   holds whatever width the panel is. */
.plot-card-thumb {
  position: relative; height: 132px; border-radius: 10px; overflow: hidden;
  border: 1px solid var(--panel-border); background: var(--panel-2);
}
.plot-card-thumb img {
  position: absolute; left: 0; top: 0; max-width: none; display: block;
}
/* Both are placed by script once the image has a size — see fillCardThumb —
   so they stay hidden until then rather than flashing at the top-left. */
.plot-card-thumb:not(.is-placed) img,
.plot-card-thumb:not(.is-placed) .plot-card-thumb-pin { opacity: 0; }
.plot-card-thumb img, .plot-card-thumb-pin { transition: opacity .12s ease; }
.plot-card-thumb-pin {
  position: absolute; left: 0; top: 0; width: 13px; height: 13px;
  margin: -6.5px 0 0 -6.5px; border-radius: 50%;
  background: var(--pin, var(--accent)); border: 2px solid #fff;
  box-shadow: 0 0 0 2px rgba(0, 0, 0, .45);
}

/* ---- the passcode prompt ---- */

.plot-ask {
  position: fixed; inset: 0; z-index: 7;
  display: flex; align-items: center; justify-content: center;
  padding: 16px;
  background: rgba(10, 12, 16, 0.72);
}
.plot-ask-box {
  width: min(360px, 100%);
  padding: 20px;
  background: var(--panel); border: 1px solid var(--panel-border); border-radius: 12px;
  display: flex; flex-direction: column; gap: 10px;
}
.plot-ask-box .subtitle { margin: 0; }
.plot-ask-box input { width: 100%; }
.plot-ask-err { font-size: 12px; color: var(--bad); min-height: 16px; }

/* ---- named plot versions ----
   Same modal shape as the passcode prompt just above, on purpose: saving,
   restoring and deleting a version are all things you mean to do, not
   something to leave open behind the map the way the wire picker sits. */
.plot-versions {
  position: fixed; inset: 0; z-index: 7;
  display: flex; align-items: center; justify-content: center;
  padding: 16px;
  background: rgba(10, 12, 16, 0.72);
}
.plot-versions-box {
  width: min(400px, 100%);
  max-height: 80vh;
  padding: 20px;
  background: var(--panel); border: 1px solid var(--panel-border); border-radius: 12px;
  display: flex; flex-direction: column; gap: 10px;
}
.plot-versions-head { display: flex; align-items: flex-start; justify-content: space-between; gap: 10px; }
.plot-versions-head h1 { margin: 0; }
.plot-versions-box .subtitle { margin: 0; }
.plot-versions-err { font-size: 12px; color: var(--bad); min-height: 16px; }
.plot-versions-list { overflow-y: auto; display: flex; flex-direction: column; gap: 8px; }
.plot-versions-cap-line { margin: 0; font-size: 11px; color: var(--dim); }
.plot-version-row {
  display: flex; align-items: center; justify-content: space-between; gap: 10px;
  padding: 8px 10px; border-radius: 8px;
  background: var(--panel-2); border: 1px solid var(--panel-border);
}
.plot-version-info { min-width: 0; }
.plot-version-name {
  font-weight: 600; font-size: 13px;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.plot-version-when { font-size: 11px; color: var(--dim); }
.plot-version-row.is-current { border-color: var(--accent); }
.plot-version-tag {
  display: inline-block; margin-left: 6px; padding: 0 6px; vertical-align: middle;
  border: 1px solid var(--accent); border-radius: 8px;
  font-size: 10px; line-height: 15px; font-weight: 600; color: var(--accent);
}
.plot-versions-current { font-size: 12px; color: var(--dim); margin: 6px 0 4px; }

/* The Gear tab's version picker: one <select> of the saved plot-and-kit
   versions, above the totals, since a version is both of them at once. */
.gear-version { margin-bottom: 14px; }
.gear-version label { display: block; font-size: 11px; text-transform: uppercase; letter-spacing: 0.04em; color: var(--dim); margin-bottom: 5px; }
.gear-version-now { font-size: 13px; margin-bottom: 6px; }
.gear-version select { width: 100%; }
.gear-version .help { margin-top: 5px; }
.plot-version-actions { flex: none; display: flex; gap: 6px; }
.plot-versions-foot { display: flex; justify-content: flex-end; }

/* The same version panel is reparented into Map above the desktop breakpoint.
   Only its presentation changes here: its list, error region, save button and
   delegated operation handlers remain the modal's original nodes. */
.plot-versions-box.is-inline {
  width: 100%; max-height: none;
  padding: 0;
  background: transparent; border: 0; border-radius: 0;
  gap: 8px;
}
.plot-versions-box.is-inline .plot-versions-head h1 { font-size: 14px; }
.plot-versions-box.is-inline .plot-version-row {
  align-items: flex-start;
  flex-direction: column;
  gap: 7px;
}
.plot-versions-box.is-inline .plot-version-actions,
.plot-versions-box.is-inline .plot-versions-foot { width: 100%; }
.plot-versions-box.is-inline .plot-version-actions button,
.plot-versions-box.is-inline .plot-versions-save { flex: 1; }

/* ---- display mode (?display=1) ----
   A wall screen with nobody standing at it: no top bar, no Unlock, no way in
   at all, so everything up there simply goes away rather than sitting there
   as chrome nobody can use. Read alongside plot.html's own DISPLAY_MODE
   comment, which explains what a touch on this screen does and does not do. */
body.plot-display .plot-top { display: none; }
body.plot-display .plot-stage { background: #000; }
/* Read from across a room rather than held in a hand, so labels alone are
   sized up — the items themselves already read fine at the zoom a wall
   settles on. Wins over the shared .plot-item-scale rule on specificity, not
   on ordering, so it can't be knocked out by a future reshuffle of this file. */
body.plot-display .plot-label .plot-item-scale { transform: scale(calc(var(--inv, 1) * 1.4)); }

/* ---- embedded in the planner's Plot tab (?embed=1) ----
   The planner's own header, its back link and its row of tabs are already on
   screen immediately above this frame, so the bar's arrow and titles would be a
   second set of all three. The buttons stay and move right: Fit, Find me,
   Layers and Unlock have no other home on this page, and the tab has to be the
   same plot as the standalone one rather than a reduced copy of it. The
   gradient stays with them — they are transparent ghost buttons sitting over a
   site map, and they have to stay legible over whatever it happens to be. */
body.plot-embed .plot-back,
body.plot-embed .plot-titles { display: none; }
/* Flat 10px rather than the standalone page's safe-area inset: a frame parked
   under the planner's tabs is nowhere near the notch, and paying for one twice
   on the same screen leaves a visible gap of nothing. */
body.plot-embed .plot-top { justify-content: flex-end; padding-top: 10px; }

/* The top bar's own connection pill goes with the rest of the bar, so a
   display screen that has gone stale gets this instead — full width, top of
   screen, in the one colour this page otherwise reserves for real trouble. */
.plot-display-offline {
  position: fixed; z-index: 6; top: 0; left: 0; right: 0;
  padding: 10px 14px calc(10px + env(safe-area-inset-top, 0px));
  background: var(--bad); color: #fff;
  font-size: 13px; font-weight: 700; text-align: center; letter-spacing: 0.02em;
}
.plot-display-offline[hidden] { display: none; }

@media (max-width: 640px) {
  /* The event name is the one title worth its width here; the plot is doing
     the explaining. */
  .plot-sub { display: none; }
  .plot-legend-slot { left: 10px; right: 10px; }
  /* Ten chips in five wrapped families is 276px of an 812px phone — a third of
     the screen, and the map is the thing you have to be able to reach to run a
     cable across it. So on a phone the picker is one scrolling strip instead,
     the same shape and for the same reason as the kinds row below it. */
  .plot-wires-body {
    flex-wrap: nowrap;
    overflow-x: auto; overscroll-behavior-x: contain;
    scrollbar-width: none;
  }
  .plot-wires-body::-webkit-scrollbar { display: none; }
  .plot-wire-group { flex: none; flex-wrap: nowrap; }
  .plot-wire-chip { flex: none; }

  /* The corner card above is sized for a laptop, where there's a whole map
     beside it to spare. On a phone `fit()` centres the map in whatever room
     is left after the top bar, which routinely leaves it nowhere near an
     edge — a fixed-height card pinned to a corner lands ON the map rather
     than beside it. Docked to the bottom instead, the same way `.plot-sheet`
     already is: full width, capped short, and its own chips (not a stacked
     list — see layersPanelHtml's comment) are what keep it short enough to
     actually clear the map rather than merely being anchored differently. */
  .plot-layers {
    top: auto; right: 0; left: 0;
    bottom: calc(12px + env(safe-area-inset-bottom, 0px));
    width: auto;
    max-height: 42vh;
    gap: 6px;
    padding: 10px;
    border-radius: 14px 14px 0 0;
    box-shadow: 0 -12px 30px rgba(0, 0, 0, 0.45);
  }
  /* Editing puts the palette (and maybe the wire picker) along the true
     bottom edge — the same clearance `.plot-sheet` gives them, for the same
     reason: without it the two bottom sheets would sit on top of each
     other. */
  body.plot-editing .plot-layers {
    bottom: calc(74px + var(--plot-wires-h, 0px) + env(safe-area-inset-bottom, 0px));
  }
  /* Trimmed for height, not just moved: measured against the actual dev
     event's map (a wide, short image that `fit()` centres with real room
     above and below it on a phone), the corner card's own content — the
     "Item kinds" heading plus its usual gaps — was still tall enough to land
     on two pins even docked at the bottom. The grouping the heading names is
     already visible from the row break above it, so it costs nothing to drop
     here and buys back the height that made the difference. */
  .plot-layers-sub { display: none; }
  .plot-layers-body { gap: 4px; }
  .plot-dim-row { padding-top: 4px; }

  /* Same bottom-dock override as .plot-layers just above, and the same
     reason: a fixed-height corner card lands on the map itself once `fit()`
     has centred it with room on every side, on a phone. Unlike Layers, whose
     content (six checkboxes) happens to be short enough to land well clear
     of the map at 42vh without ever asking, a camera list has no such luck —
     the seeded dev event's nine cameras fill 42vh outright and, measured
     against the real dev server at 375x812 (event w0r15zd), overlapped the
     fitted map by 42.68% of its area. 28vh is the value that cleared it with
     room to spare in that same measurement (map bottom at y:555, panel top
     at y:572.6) — smaller than Layers needs, because this list scrolls past
     its cap far more often than Layers' six toggles ever do, so there is no
     harm in it showing fewer rows before that scroll kicks in. */
  .plot-cameras {
    top: auto; right: 0; left: 0;
    bottom: calc(12px + env(safe-area-inset-bottom, 0px));
    width: auto;
    max-height: 28vh;
    gap: 6px;
    padding: 10px;
    border-radius: 14px 14px 0 0;
    box-shadow: 0 -12px 30px rgba(0, 0, 0, 0.45);
  }
  body.plot-editing .plot-cameras {
    bottom: calc(74px + var(--plot-wires-h, 0px) + env(safe-area-inset-bottom, 0px));
  }
}

/* A panel over the bottom half of a phone is a sheet; over the bottom half of
   a laptop it is a waste of a screen that has room beside the map. */
@media (min-width: 800px) {
  .plot-sheet, .plot-sheet.is-card {
    /* Below the top bar rather than behind it: the bar holds Fit and Done, and
       a panel that covers the way out of edit mode is a trap. */
    left: auto; right: 0; top: 58px; bottom: 0;
    width: 340px; max-height: none;
    border-radius: 0; border: none; border-left: 1px solid var(--panel-border);
    box-shadow: -12px 0 30px rgba(0, 0, 0, 0.45);
  }
  body.plot-sheet-open .plot-palette { right: 340px; }
  /* The picker stops short of the panel for the palette's reason, and the
     panel is a column down the right here rather than a sheet across the
     bottom — so it is no longer competing with the picker for height. */
  body.plot-sheet-open .plot-wires { right: 340px; }
  body.plot-wires-open .plot-sheet { max-height: none; }
  /* The layers panel is anchored to the same corner the item sheet opens
     into, so it steps aside rather than sitting on top of it. */
  body.plot-sheet-open .plot-layers { right: 352px; }
  body.plot-sheet-open .plot-cameras { right: 352px; }
}

/* ---- the docked tool column (>= 900px) ----
   Read plot.html's own comment above #plot-tools first: this is the styling
   for a column whose CONTENTS are the same nodes the phone layout uses,
   relocated into it rather than duplicated.

   320px is Assign's `.assign-side`, deliberately — the plot is the third page
   in this app to dock a working column beside the thing it acts on, and a
   third slightly different width would be a third answer to a question already
   settled twice. 900px is Setup's and Assign's breakpoint, for the same
   reason. */
:root { --plot-tools-w: 320px; }

.plot-tools {
  position: fixed; z-index: 5;
  top: 0; right: 0; bottom: 0;
  width: var(--plot-tools-w);
  display: flex; flex-direction: column;
  background: var(--panel-2);
  border-left: 1px solid var(--panel-border);
  border-radius: 14px;
  overflow: hidden;
}
/* The column scrolls, not the page — .plot-body is overflow:hidden, and the
   camera list is as long as the plot is busy. */
/* The two sittings — building the plot, or reading it. Outside the scrolling
   body on purpose (see the markup), so switching back is always one press away
   however far down the column you are, and whatever has replaced it. */
.plot-tools-tabs {
  display: grid; grid-template-columns: 1fr 1fr; gap: 4px; flex: none;
  padding: calc(10px + env(safe-area-inset-top, 0px)) 12px 0;
}
/* An author `display` rule beats the UA stylesheet's [hidden] one on
   specificity ties regardless of source order, so the standalone link's
   `.hidden = true` (plot.html) would do nothing without this — same fix
   .plot-view-controls[hidden] already needed below. */
.plot-tools-tabs[hidden] { display: none; }
.plot-tools-tab {
  padding: 7px 10px; border-radius: 9px; cursor: pointer;
  border: 1px solid transparent; background: var(--panel);
  color: var(--dim); font-size: 12px; font-weight: 700;
}
.plot-tools-tab:hover { color: var(--text); }
.plot-tools-tab.is-on { background: var(--accent); border-color: var(--accent); color: #fff; }
/* The body's own top padding is the tab strip's job now — two lots of it
   pushed the first heading a third of the way down the column. */
.plot-tools-tabs + .plot-tools-body { padding-top: 10px; }

.plot-tools-body {
  flex: 1; min-height: 0; overflow-y: auto;
  padding: calc(10px + env(safe-area-inset-top, 0px)) 12px calc(12px + env(safe-area-inset-bottom, 0px));
}
.plot-tools-sec { padding: 4px 0 16px; }
.plot-tools-sec + .plot-tools-sec { border-top: 1px solid var(--panel-border); padding-top: 16px; }
.plot-tools-sec > summary { display: flex; align-items: center; cursor: pointer; list-style: none; margin-bottom: 10px; }
.plot-tools-sec > summary::-webkit-details-marker { display: none; }
.plot-tools-sec > summary::after { content: "⌃"; margin-left: auto; color: var(--dim); transition: transform .18s ease; }
.plot-tools-sec:not([open]) > summary::after { transform: rotate(180deg); }
.plot-tools-sec > summary h2 {
  margin: 0; color: var(--dim); font-size: 11px; font-weight: 700;
  letter-spacing: .08em; text-transform: uppercase;
}
.plot-tools-sec[open] > :not(summary) { animation: plot-section-in .18s ease; }
@keyframes plot-section-in { from { opacity: 0; transform: translateY(-4px); } to { opacity: 1; transform: none; } }

@media (min-width: 900px) {
  /* Every rule here is gated on body.plot-tools-on, which JS sets only when
     the column is actually in use — never on a wall display. Without the gate,
     a 1200px display screen would have its map stop short of a column that
     isn't there. */

  /* Not embedded: the planner's Plot tab already has its own header naming
     the event above the frame, so showing this too would just be the same
     line twice on screen. */
  body.plot-tools-on:not(.plot-embed) .plot-map-info { display: block; }

  /* The map's own box ends where the column begins, rather than running under
     it. This is the whole difference between a docked panel and a floating
     one: fit(), the pan clamp and every client-to-map coordinate on the page
     are computed from .plot-stage's size, so insetting the stage means none of
     them has to learn about the column separately, and nothing can be dropped
     or centred underneath it. */
  body.plot-tools-on .plot-stage { top: 12px; right: calc(var(--plot-tools-w) + 24px); bottom: 12px; left: 12px; border-radius: 14px; overflow: hidden; }
  body.plot-tools-on .plot-tools { top: 12px; right: 12px; bottom: 12px; }
  /* Both of these are positioned against the window, so they follow the map's
     new right edge rather than hiding behind the column. */
  body.plot-tools-on .plot-follow { right: calc(var(--plot-tools-w) + 12px); }
  /* The map's own corners are inside the map, not the window — and INSIDE is
     the operative word. `--plot-tools-w + 24px` is exactly where the stage's
     own right edge is (see its rule above), so these sat flush against it
     with no margin at all, while the top-right button was 52px down from the
     card's top edge. Even gaps on both axes is the whole ask: 12px inside the
     card, matching the 12px the card itself is inset from the window. */
  body.plot-tools-on .plot-view-controls,
  body.plot-tools-on .plot-map-controls,
  body.plot-tools-on .plot-versions-drop,
  body.plot-tools-on .plot-layers { right: calc(var(--plot-tools-w) + 36px); }
  /* 24 = the card's own 12px top inset, plus 12 again inside it. The 64px the
     phone layout uses is clearance for a top bar that is not on screen up
     here — it moved into the column. */
  body.plot-tools-on .plot-map-controls { top: calc(24px + env(safe-area-inset-top, 0px)); }
  /* And the card hangs under its button: 24 + the button's 34 + a 6px gap. */
  body.plot-tools-on .plot-layers,
  body.plot-tools-on .plot-versions-drop { top: calc(64px + env(safe-area-inset-top, 0px)); }
  body.plot-tools-on .plot-flash { left: calc(50% - var(--plot-tools-w) / 2); }
  body.plot-tools-on .plot-note { left: calc(50% - var(--plot-tools-w) / 2); }
  /* Everything the palette held is in the column now. */
  body.plot-tools-on .plot-palette { display: none; }
  /* Scale and Upload map are two halves of the same job, and they share one
     wrapping row rather than stacking as the two palette groups they arrive
     as. The groups keep their own identities — they are still what TOOL_MOVES
     relocates — they just sit side by side inside it. */
  body.plot-tools-on .plot-map-actions {
    display: flex; flex-wrap: wrap; gap: 6px; align-items: center;
  }
  body.plot-tools-on .plot-map-actions > * { display: contents; }
  /* Cameras is still a card this column absorbs, and a button that opens
     something already on screen is a button that appears to do nothing.
     Layers is NOT in that set any more — its button lives on the map and its
     card stays a card at every width, so hiding it here left the new control
     invisible on exactly the layout it was added for. */
  body.plot-tools-on #plot-cameras-btn { display: none; }
  body.plot-embed #plot-unlock { display: none !important; }

  /* The relocated cards stop being cards. They keep their own contents and
     their own class names — they are the same nodes, and below the breakpoint
     they are corner panels again — but inside a section they are just content:
     no second background, no second border, no scroll box of their own inside
     the column's. */
  /* .plot-layers is deliberately NOT in this set any more: it never relocates,
     so it is still a corner card up here and must keep its own box. Stripping
     its background and position left it stretched across the whole map. */
  body.plot-tools-on .plot-cameras,
  body.plot-tools-on .plot-wires {
    position: static;
    width: auto; max-height: none; overflow: visible;
    padding: 0; gap: 10px;
    background: none; border: 0; box-shadow: none;
  }
  /* Their internal headings and closers go with them: each surrounding group
     already has a heading, and all groups remain present together. */
  /* Same exception: Layers keeps its heading and its Close, because up here
     it is still a card somebody has to be able to shut. */
  body.plot-tools-on .plot-cameras-head,
  body.plot-tools-on #plot-wires-done { display: none; }
  /* The former map-top action row is ordinary compact content in the first
     group now, not a second desktop control surface. */
  body.plot-tools-on .plot-top {
    position: static; inset: auto; min-height: 0; padding: 0;
    display: flex; flex-wrap: wrap; justify-content: flex-start; gap: 8px;
    background: none; border: 0; box-shadow: none;
  }
  /* Titles go with Back: Plot controls is Build-only, and the event name now
     lives in .plot-map-info instead (see its own rule), floating over the
     map itself so it stays on screen whichever tab is open — including on
     the standalone link, which no longer has a Build tab to hold it. */
  body.plot-tools-on .plot-top .plot-back,
  body.plot-tools-on .plot-top .plot-titles { display: none; }
  body.plot-tools-on .plot-top .plot-conn { flex-basis: 100%; }
  body.plot-tools-on #plot-sec-controls .plot-top { justify-content: flex-start; gap: 6px; }
  body.plot-tools-on #plot-sec-controls .plot-top .plot-conn { flex-basis: 100%; padding: 3px 7px; }
  body.plot-tools-on #plot-sec-controls .plot-utility-btn {
    display: inline-flex; flex: 1 1 calc(50% - 4px); width: auto; min-width: 0;
    justify-content: center; gap: 6px; padding: 0 8px;
  }
  body.plot-tools-on #plot-sec-controls .plot-utility-label { display: inline; }
  body.plot-tools-on #plot-sec-controls .plot-top #plot-pal-present { display: flex; width: 100%; gap: 6px; }
  /* The kinds strip scrolls sideways on a phone because six of them will not
     fit across 375px. In a 320px column with two rows to spare they simply
     wrap, which is the shape that shows all six at once. */
  body.plot-tools-on .plot-pal-kinds { flex-wrap: wrap; overflow: visible; }
  /* Real boxes now rather than `display: contents` — in here they are a group
     of buttons in a section, not part of a row somewhere else. `:not([hidden])`
     because this selector outweighs the `.plot-pal-group[hidden]` rule that
     would otherwise put a locked plot's editing tools away, and a group that
     ignores its own hidden attribute is the same class of bug the comment on
     that rule's block already describes. */
  body.plot-tools-on .plot-pal-group:not([hidden]) { display: flex; flex-wrap: wrap; gap: 8px; }
  /* The two Map groups used to be a 2x2 grid of full-width buttons stacked in
     two rows. They share one row now — see .plot-map-actions — so the groups
     themselves stop being boxes and let their buttons flow into it. This has
     to out-specify the id-scoped rule it replaces, hence naming both. */
  body.plot-tools-on #plot-sec-map .plot-map-actions > .plot-pal-group:not([hidden]) { display: contents; }
  body.plot-tools-on #plot-sec-map .plot-map-actions > .plot-pal-group[hidden] { display: none; }
  body.plot-tools-on #plot-sec-map .plot-map-actions > .plot-pal-group button { flex: 1 1 46%; }
  body.plot-tools-on #plot-sec-map .plot-pal-group button { min-width: 0; padding: 9px 8px; }
  /* One family per line: ten chips in five families read as a paragraph across
     a narrow column, which is the same objection the phone answers by
     scrolling them instead. */
  body.plot-tools-on .plot-wires-body { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; }
  body.plot-tools-on .plot-wire-group { min-width: 0; padding: 8px; border: 1px solid var(--panel-border); border-radius: 10px; background: var(--panel); }
  body.plot-tools-on .plot-wire-group:first-child { grid-column: 1 / -1; }
  body.plot-tools-on .plot-wire-family { flex-basis: 100%; margin-bottom: 2px; }
  /* No rule above it in the column: it is one of the Map section's own
     settings, not a footnote under them, and a line there made the section
     read as two. It keeps its rule on a phone, where the row genuinely is the
     tail of a card of layer toggles and needs separating from them. */
  body.plot-tools-on .plot-dim-row { padding-top: 12px; border-top: 0; }
  /* The presenter pair is on the map's own control row now, so it is a pair of
     icon buttons like the rest of that row rather than a labelled group. */
  body.plot-tools-on #plot-pal-present:not([hidden]) { display: contents; }
  /* The bottom edge is clear again: no palette, no picker down there. Both of
     these are the plain non-editing values, restated because the editing rule
     they undo is written without a breakpoint. */
  body.plot-tools-on.plot-editing .plot-legend-slot { bottom: calc(12px + env(safe-area-inset-bottom, 0px)); }
  body.plot-tools-on.plot-editing .plot-flash { bottom: calc(16px + env(safe-area-inset-bottom, 0px)); }
}

@media (min-width: 900px) {
  body.plot-tools-on.plot-sheet-open .plot-tools-body {
    display: flex; flex-direction: column; overflow: hidden;
  }
  body.plot-tools-on .plot-sheet,
  body.plot-tools-on .plot-sheet.is-card {
    position: static;
    width: 100%; max-height: none;
    flex: 1; min-height: 0;
    border: 0; border-radius: 0; box-shadow: none;
  }
  body.plot-tools-on .plot-tools-editor { min-height: 0; display: flex; flex: 1; }
  body.plot-tools-on .plot-tools-editor[hidden] { display: none; }
  body.plot-tools-on .plot-sheet-head { padding: 14px 16px; }
  body.plot-tools-on .plot-sheet-body { padding: 14px 16px 18px; }
}

/* ---------------------------------------------------------------------------
   Added by the planner work: the Crew tab's on-camera load bar, and the marks
   badge/list. Appended at the end on purpose, so this lands cleanly next to
   whatever else is being changed further up.
   --------------------------------------------------------------------------- */

/* Two bars per person under their stats: how long they are on site, and how
   long they are actually rolling, both against the standard shooting day.
   Each is a line of words with the track on its own full-width line beneath,
   so every track on the panel is the same width and (loadBarModel) the same
   scale — the words used to share the track's line and take whatever width
   they needed, which left every bar a different length and none comparable.
   The tick is the standard mark, at the same place on all of them; past it
   the fill turns red and the overage is printed in red beside the hours. */
.load-bars { margin-top: 6px; }
.cam-load {
  display: grid; grid-template-columns: 1fr auto auto;
  column-gap: 8px; row-gap: 3px; align-items: baseline; margin-top: 4px;
}
.cam-load-what { font-size: 11px; color: var(--dim); white-space: nowrap; }
.cam-load-mins, .cam-load-overby {
  font-size: 11px; white-space: nowrap; text-align: right;
  font-variant-numeric: tabular-nums;
}
.cam-load-mins { color: var(--text); }
.cam-load-overby { color: var(--bad); }
.cam-load-overby:empty { display: none; }
.cam-load.over .cam-load-mins { color: var(--bad); }
.cam-load-track {
  grid-column: 1 / -1; position: relative; height: 5px;
  background: var(--panel-3);
  border-radius: 3px; overflow: hidden;
}
.cam-load-fill {
  position: absolute; left: 0; top: 0; bottom: 0;
  background: var(--accent); border-radius: 3px;
  transition: width 0.18s ease;
}
.cam-load-over { background: var(--bad); border-radius: 0 3px 3px 0; }
.cam-load-tick {
  position: absolute; top: -1px; bottom: -1px; width: 1px;
  background: var(--text); opacity: 0.55;
}

/* Crew marks — the moments a shooter taps on their phone. Read-only here: a
   count on the block, the times in the panel that block opens. */
.mark-badge {
  display: inline-block; margin-left: 5px; padding: 0 5px;
  border: 1px solid var(--warn); border-radius: 8px;
  font-size: 10px; line-height: 14px; font-weight: 600;
  color: var(--warn); white-space: nowrap; vertical-align: middle;
}
.vtl-block .mark-badge { padding: 0 4px; font-size: 9px; line-height: 13px; }
.bm-marks { display: flex; flex-wrap: wrap; gap: 4px; margin-bottom: 6px; }
.bm-marks .mark-time {
  padding: 1px 6px; border: 1px solid var(--panel-border);
  border-radius: 10px; font-size: 11px; color: var(--dim);
  font-variant-numeric: tabular-nums;
}
