:root {
  --bg-primary: #0a0a0c;
  --bg-secondary: #17171b;
  --bg-tertiary: #232329;
  --text-primary: #f5f5f5;
  --text-secondary: #b8b8c0;
  --text-muted: #7c7c86;
  --border-color: #2c2c33;
  --brand-primary: #14b8a6;
  --status-good: #34d399;
  --status-warning: #fbbf24;
  --status-error: #f87171;
  --status-offline: #6b7280;
}

* {
  box-sizing: border-box;
}

body {
  margin: 0;
  background: var(--bg-primary);
  color: var(--text-primary);
  font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
  min-height: 100vh;
}
/* Only av/admin ever mount the fixed soundboard-footer + mini-player stack
   (av.js) - reserving this space on body itself (not just #timeline) means
   NOTHING on the page, at any scroll position, can ever end up positioned
   underneath the fixed footer and become unclickable. */
body[data-role="av"],
body[data-role="admin"] {
  padding-bottom: var(--bottom-chrome-h, 0px);
}

/* UI audit's single highest-value pre-show change: nothing on screen
   previously told the operator which event or role they were on. Sticky so
   it's always visible; #offline-banner (below) sticks just underneath it
   via --header-h, so the two sticky-top elements never fight for the same
   spot. */
#app-header {
  position: sticky;
  top: 0;
  z-index: 12;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 0.75rem;
  min-height: 44px;
  padding: 0.625rem 1rem;
  background: var(--bg-secondary);
  border-bottom: 1px solid var(--border-color);
}
#app-header[hidden] {
  display: none;
}
#app-header-name {
  font-weight: 700;
  font-size: 1.0625rem;
  color: var(--text-primary);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
#app-header-role {
  flex-shrink: 0;
  padding: 0.1875rem 0.625rem;
  border-radius: 999px;
  background: var(--brand-primary);
  color: var(--bg-primary);
  font-size: 0.75rem;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.03em;
}

#offline-banner {
  position: sticky;
  top: var(--header-h, 0px);
  z-index: 10;
  width: 100%;
  padding: 0.5rem 1rem;
  background: var(--status-offline);
  color: var(--text-primary);
  font-weight: 600;
  text-align: center;
}
#offline-banner[data-state="online"] {
  background: var(--status-good);
  color: var(--bg-primary);
}
#offline-banner[hidden] {
  display: none;
}

#session-recovery {
  padding: 3rem 1.5rem;
  text-align: center;
}
#session-recovery[hidden] {
  display: none;
}
#session-recovery h2 {
  margin: 0 0 0.75rem;
  font-size: 1.375rem;
  color: var(--status-warning);
}
#session-recovery p {
  margin: 0;
  color: var(--text-secondary);
  font-size: 1rem;
  line-height: 1.5;
}

#drift-strip {
  padding: 0.5rem 1rem;
  font-size: 0.9375rem;
  font-weight: 600;
  color: var(--text-secondary);
}
#drift-strip.drift-warning {
  color: var(--status-warning);
}

#timeline {
  list-style: none;
  margin: 0;
  /* Reserves room for the fixed/sticky bottom chrome stack (mini player +
     soundboard footer + admin bar), whose combined real height is measured
     and written into --bottom-chrome-h by av.js/admin.js - never a guess. */
  padding: 0 1rem calc(1.5rem + var(--bottom-chrome-h, 6rem));
}

.row {
  padding: 0.75rem 1rem;
  min-height: 44px;
  border-bottom: 1px solid var(--border-color);
  background: var(--bg-secondary);
  /* The sticky #app-header sits on top of scrolled content - without this,
     renderTimeline's auto-scrollIntoView (or a user's own scroll) can leave
     a row tucked exactly underneath it: visible but unclickable. */
  scroll-margin-top: calc(var(--header-h, 0px) + 8px);
}

.row[data-state="current"] {
  background: var(--bg-secondary);
  border-left: 4px solid var(--brand-primary);
  box-shadow: 0 1px 3px rgba(0, 0, 0, 0.4);
}
.row[data-state="current"] .row-title {
  font-size: 2rem;
  font-weight: 700;
  line-height: 1.15;
}

.row[data-state="next"] {
  border-left: 2px solid var(--brand-primary);
}
.row[data-state="next"] .row-title {
  font-size: 1.25rem;
  font-weight: 600;
}
/* UI audit: next-vs-upcoming used to differ only by a 2px-vs-4px border,
   imperceptible on a dim phone. An explicit eyebrow tag makes "what's
   coming up" legible at a glance in a dark room. */
.row[data-state="next"]::before {
  content: "NEXT";
  display: block;
  margin-bottom: 0.125rem;
  color: var(--brand-primary);
  font-size: 0.6875rem;
  font-weight: 700;
  letter-spacing: 0.08em;
}

.row[data-state="past"] .row-title {
  font-size: 1rem;
  font-weight: 500;
  color: var(--text-secondary);
  opacity: 0.45;
}

.row[data-state="upcoming"] .row-title {
  font-size: 1rem;
  font-weight: 500;
}

/* Owner: "we don't need transitions... just the name of the act" - display
   filter only, no data removed (see timeline.js). Host screen keeps every
   transition visible since that's how hosts reach their transition scripts
   (host-script.js's "View script" button lives on this same row). A
   transition that is the live item never gets this class in the first
   place (timeline.js), so the live cursor is never hidden on any screen. */
.row-transition-hidden {
  display: none;
}
body[data-role="host"] .row-transition-hidden,
body[data-role="admin"] .row-transition-hidden,
body[data-role="av"] .row-transition-hidden {
  /* Owner directive: admin and av are trusted crew and see every item type
     on every screen, transitions included. Only participant/volunteer/guest
     get the acts-only filtered list. Pure display toggle (CSS class from
     timeline.js, unchanged) - no count assumption either side. */
  display: block;
}

.row-collapsed {
  max-height: 1.5rem;
  overflow: hidden;
  padding: 0.25rem 1rem;
  transition: max-height 0.2s ease;
}
.row-collapsed .row-title {
  font-size: 0.8125rem;
  font-weight: 400;
  color: var(--text-muted);
}

#jump-to-now {
  position: sticky;
  bottom: 1rem;
  left: 50%;
  margin-left: -60px;
  min-width: 120px;
  min-height: 44px;
  padding: 0.5rem 1rem;
  border-radius: 999px;
  border: none;
  background: var(--brand-primary);
  color: var(--bg-primary);
  font-weight: 600;
}

/* Stacks ABOVE the mini player + soundboard footer (--soundboard-h /
   --miniplayer-h, measured in av.js) rather than a guessed constant, so
   this bar and the soundboard never overlap on any device. */
#admin-bar {
  position: sticky;
  bottom: calc(var(--soundboard-h, 0px) + var(--miniplayer-h, 0px));
  z-index: 20;
  display: none;
  gap: 1rem;
  padding: 0.75rem 1rem;
  background: var(--bg-tertiary);
  border-top: 1px solid var(--border-color);
}
#admin-bar[data-visible="true"] {
  display: flex;
}
#admin-bar button {
  min-width: 44px;
  min-height: 44px;
  padding: 0.5rem 1rem;
  border-radius: 6px;
  font-weight: 600;
}
/* UI audit: Back/Next used to be visual twins ~8px apart - a mis-tap risk.
   Next is the primary, filled action; Back is a secondary/ghost button, and
   the gap above is now >=16px (see #admin-bar's gap:1rem). */
#admin-bar .admin-bar-primary {
  background: var(--brand-primary);
  color: var(--bg-primary);
  border: none;
}
#admin-bar .admin-bar-secondary {
  background: transparent;
  color: var(--text-primary);
  border: 1px solid var(--border-color);
}
#admin-bar button:disabled {
  opacity: 0.5;
}
.toast {
  padding: 0.5rem;
  color: var(--status-warning);
  font-size: 0.875rem;
}

/* Show-pack download panel (UI audit fix - was completely unstyled default
   browser chrome, the only such control left on the AV/admin/host screens). */
#showpack-panel {
  padding: 1rem;
  border-bottom: 1px solid var(--border-color);
}
#download-showpack {
  min-height: 44px;
  padding: 0.625rem 1.25rem;
  background: var(--brand-primary);
  color: var(--bg-primary);
  border: none;
  border-radius: 8px;
  font-weight: 700;
  font-size: 1rem;
}
#download-showpack:disabled {
  background: var(--bg-tertiary);
  color: var(--text-muted);
  border: 2px dashed var(--border-color);
  opacity: 1;
  cursor: not-allowed;
}
#download-progress {
  margin-top: 0.5rem;
  color: var(--text-secondary);
  font-size: 0.9375rem;
}
#readiness-badges {
  margin-top: 0.375rem;
  color: var(--text-secondary);
  font-size: 0.875rem;
}
#not-show-ready-list h3 {
  margin: 1rem 0 0.5rem;
  font-size: 0.9375rem;
  color: var(--text-secondary);
}
#not-show-ready-list ul {
  margin: 0;
  padding-left: 1.25rem;
  color: var(--text-muted);
  font-size: 0.875rem;
}

/* Host: view-script icon-button on every host-role row */
.host-view-script-btn {
  min-width: 44px;
  min-height: 44px;
  margin-left: 0.5rem;
  padding: 0.25rem 0.75rem;
  background: transparent;
  color: var(--brand-primary);
  border: 1px solid var(--brand-primary);
  border-radius: 6px;
  font-size: 0.8125rem;
}

.host-own-row {
  cursor: pointer;
}

/* Host full-screen script view (07-CONTEXT.md Decision 6) */
#host-script-overlay {
  position: fixed;
  inset: 0;
  /* Above #wakelock-banner (z-index 30, 07-04/07-05's D-13 failure banner) -
     found via 07-08's real-Chromium screenshot pass: when both are visible
     at once, the overlay's own "< Timeline" back button was completely
     hidden underneath the banner (stacking bug, invisible to the automated
     DOM assertion since the element was still technically "visible" by
     bounding-box/display checks - only caught by actually looking at the
     screenshot). A full-screen view's own controls must always be
     reachable while it's open; the persistent global banner reappears once
     the overlay closes. */
  z-index: 40;
  display: flex;
  flex-direction: column;
  background: var(--bg-primary);
  /* Now sits BELOW the soundboard/mini-player (z-index 41/42) for admin,
     which stay reachable on top of this overlay - reserve room so the
     sticky #host-script-footer isn't hidden underneath them. */
  padding-bottom: var(--bottom-chrome-h, 0px);
}

#host-script-back {
  min-height: 44px;
  padding: 0.75rem 1rem;
  align-self: flex-start;
  background: transparent;
  color: var(--text-primary);
  border: none;
  font-size: 1.125rem;
  font-weight: 600;
}

#host-script-body {
  flex: 1;
  overflow-y: auto;
  padding: 1rem 1.5rem 2rem;
  font-size: 1.75rem;
  line-height: 1.5;
  color: var(--text-primary);
  white-space: pre-wrap;
}

#host-script-footer {
  position: sticky;
  bottom: 0;
  padding: 0.75rem 1.5rem;
  background: var(--bg-tertiary);
  border-top: 1px solid var(--border-color);
  color: var(--text-secondary);
  font-weight: 600;
}

/* AV iPad-landscape grid (07-CONTEXT.md Decision 7) - >=900px: 320px left
   column (shared timeline, narrow variant) + right column is the cue/
   transport panel. The soundboard is its own fixed footer now (styled
   above), not a grid participant, so this is a single row. Below 900px:
   single column, phone-portrait fallback. */
body[data-role="av"] {
  display: grid;
  grid-template-columns: 320px 1fr;
  grid-template-rows: 1fr;
  height: 100vh;
  margin: 0;
}
body[data-role="av"] #app-header {
  grid-column: 1 / -1;
  grid-row: 1;
  /* Without this, a grid item stretches to fill its whole row by default -
     in AV's single-row grid that means the whole viewport height, silently
     intercepting every click underneath it. Pin it to its own content
     height at the top instead. */
  align-self: start;
  z-index: 13;
}
body[data-role="av"] #offline-banner {
  grid-column: 1 / -1;
  grid-row: 1;
  align-self: start;
  margin-top: var(--header-h, 0px);
  z-index: 11;
}
body[data-role="av"] #drift-strip {
  grid-column: 1;
  grid-row: 1;
  align-self: start;
  margin-top: var(--header-h, 0px);
}
body[data-role="av"] #timeline {
  grid-column: 1;
  grid-row: 1 / -1;
  overflow-y: auto;
  /* UI audit fix: was a hardcoded 3rem guess, never enough once a wake-lock
     banner (variable height, appears/disappears at runtime) rendered above
     it - the program list's first rows ended up clipped underneath the
     banner. Now reads the real measured top-stack height av.js writes
     (syncTopChromeVars), with 3rem kept only as the pre-measurement
     fallback so there is no flash-of-wrong-padding before JS runs. */
  padding-top: var(--top-chrome-h, 3rem);
}
body[data-role="av"] #wakelock-banner {
  grid-column: 1 / -1;
  grid-row: 1;
  align-self: start;
  margin-top: var(--header-offline-h, var(--header-h, 0px));
  z-index: 14;
}
/* UI audit fix: the SAME implicit-row/overlap bug as #wakelock-banner just
   above, on a second, independent banner (app.js's warnIfNotStandalone) -
   this is in fact the exact banner text from the owner's screenshot ("Add
   this to your Home Screen before downloading..."), not the wake-lock one.
   Stacks below the wake-lock banner (av.js's syncTopChromeVars). */
body[data-role="av"] #standalone-warning {
  grid-column: 1 / -1;
  grid-row: 1;
  align-self: start;
  margin-top: var(--header-offline-wakelock-h, var(--header-h, 0px));
  z-index: 15;
}
/* UI audit fix: below the AV grid breakpoint, body[data-role="av"] switches
   to plain block layout (the max-width:899px block further down) - the
   grid-only stacking margin-top values just above (offset from a shared
   grid-cell origin, valid only when these banners actually overlap in one
   cell) become ordinary block-flow margins in that context, which are
   ADDITIVE to wherever the previous element already rendered - opening a
   growing dead gap between the two banners that gets worse the more of
   them are visible. Placed here (after the base rules, same selector
   specificity) so source order - not just the media query - picks this
   reset within its own range; a copy living earlier in the file lost to
   the unconditional rule above regardless of the media query, which is
   the exact bug this replaces. In block flow, DOM order alone already
   stacks the banners correctly with zero extra margin needed. */
@media (max-width: 899px) {
  body[data-role="av"] #wakelock-banner,
  body[data-role="av"] #standalone-warning {
    margin-top: 0;
  }
}
body[data-role="av"] #timeline .row-title {
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
}
body[data-role="av"] #jump-to-now {
  display: none !important;
}
#av-right-column {
  grid-column: 2;
  grid-row: 1;
  display: flex;
  flex-direction: column;
  overflow-y: auto;
  /* UI audit fix: this column shares row 1 with the wake-lock/standalone
     banners exactly like #timeline does, but only #timeline's own top
     offset was synced to --top-chrome-h - this column had none, so its
     first real control (#av-transport, the main Play button) rendered
     directly underneath whichever banner was showing and silently ate its
     clicks (caught by the real-browser soundboard/offline-gating Playwright
     specs, which click that exact button). Same var, same fix. */
  padding-top: var(--top-chrome-h, 0px);
  padding-bottom: calc(1rem + var(--bottom-chrome-h, 0px));
}
#av-cue-panel {
  padding: 1rem;
}
#av-cue-panel h2 {
  font-size: 1.5rem;
}
#av-transport {
  min-width: 96px;
  min-height: 96px;
  font-size: 1.5rem;
  font-weight: 700;
  background: var(--brand-primary);
  color: var(--bg-primary);
  border: none;
  border-radius: 12px;
}
/* Pre-show-rehearsal caveat fix (2026-08-14): a plain opacity dim on an
   already-dark teal button was genuinely hard to read as "disabled" at a
   glance in a dark room (docs/ops/PRESHOW-REHEARSAL-2026-08-14.md). Disabled
   now swaps to the neutral/muted palette already used elsewhere for "not
   ready" states (soundboard-pad[data-readiness], av-readiness-badge) plus a
   dashed border, so it reads as visually distinct from the solid brand-teal
   enabled state, not just dimmer. */
#av-transport:disabled {
  background: var(--bg-tertiary);
  color: var(--text-muted);
  border: 2px dashed var(--border-color);
  opacity: 1;
  cursor: not-allowed;
}
#av-transport-hint {
  margin-top: 0.5rem;
  color: var(--text-muted);
  font-size: 0.8125rem;
}
#av-send-to-display {
  min-height: 44px;
  margin-left: 1rem;
  padding: 0.5rem 1rem;
  background: transparent;
  color: var(--brand-primary);
  border: 1px solid var(--brand-primary);
  border-radius: 6px;
}
/* Owner UX spec: soundboard is a FIXED STICKY FOOTER, always visible, never
   scrolling away with the program - not the AV-grid-only panel it used to
   be. Same element/module for every role that mounts it (av.js), single
   page, role-gated by who app.js chooses to mount it for. Collapses to a
   one-tap icon (#av-soundboard-header) so the program stays readable;
   expands to the full grid on demand - state persisted per-device
   (av.js's isSoundboardExpanded). */
#av-soundboard-area {
  position: fixed;
  left: 0;
  right: 0;
  bottom: 0;
  /* Owner: "must never become unreachable because of what else is on
     screen" - must outrank #host-script-overlay/#admin-detail-overlay
     (z-index 40, full-viewport inset:0), since admin mounts both of those
     ON TOP of the same AV panel that owns this soundboard (admin is a
     superset of every role's screen, app.js). Without this, opening the
     item detail panel or expanding the host script fullscreen completely
     covered and blocked the soundboard for admin. */
  z-index: 41;
  max-height: 45vh;
  overflow-y: auto;
  background: var(--bg-secondary);
  border-top: 1px solid var(--border-color);
}
#av-soundboard-header {
  display: flex;
  justify-content: flex-end;
  padding: 0.375rem 0.75rem;
}
#av-soundboard-toggle {
  min-width: 44px;
  min-height: 44px;
  padding: 0.375rem 0.75rem;
  background: var(--bg-tertiary);
  color: var(--text-primary);
  border: 1px solid var(--border-color);
  border-radius: 8px;
  font-size: 1.25rem;
}
#av-soundboard-area[data-expanded="false"] #av-soundboard-grid {
  display: none;
}
#av-soundboard-area[data-expanded="true"] #av-soundboard-grid {
  display: grid;
  /* Responsive, not a fixed 3 columns - stays comfortably >=44px tappable
     from a 390px phone up through a 1440px desktop sidebar. */
  grid-template-columns: repeat(auto-fill, minmax(88px, 1fr));
  gap: 0.5rem;
  padding: 0 0.75rem 0.75rem;
}
.soundboard-pad {
  min-height: 72px;
  min-width: 72px;
  border-radius: 8px;
  border: 1px solid var(--border-color);
  background: var(--pad-color, var(--bg-tertiary));
  color: var(--text-primary);
  font-weight: 600;
  font-size: 0.9375rem;
  padding: 0.5rem;
}
/* Owner UX spec: while audio is playing, a minimized player stays visible
   directly above the soundboard footer regardless of scroll position or
   whether the soundboard is expanded/collapsed - AV must never lose track
   of, or have to hunt for, what's currently playing. Reuses av.js's
   existing shared media elements (mainAudio/previewAudio/pad audio) - not
   a second player. */
#av-mini-player {
  position: fixed;
  left: 0;
  right: 0;
  bottom: var(--soundboard-h, 0px);
  /* Same reason as #av-soundboard-area above - stay above the fullscreen
     overlays (z-index 40), one higher than the soundboard itself. */
  z-index: 42;
  display: flex;
  align-items: center;
  gap: 0.75rem;
  padding: 0.5rem 0.75rem;
  background: var(--bg-tertiary);
  border-top: 1px solid var(--brand-primary);
}
#av-mini-player[hidden] {
  display: none;
}
#av-mini-player-label {
  flex: 1;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  font-weight: 600;
  font-size: 0.875rem;
}
#av-mini-player button {
  min-width: 44px;
  min-height: 40px;
  padding: 0.375rem 0.75rem;
  background: var(--brand-primary);
  color: var(--bg-primary);
  border: none;
  border-radius: 6px;
  font-weight: 600;
  font-size: 0.8125rem;
}
#av-mini-player-stop {
  background: transparent;
  color: var(--status-error);
  border: 1px solid var(--status-error);
}

/* "Set as current act" - the deliberate live-cursor control, distinct from
   browsing. Per-row (av.js) and in the admin detail panel (admin.js). */
#admin-detail-set-current {
  min-height: 44px;
  padding: 0.5rem 1rem;
  background: transparent;
  color: var(--status-warning);
  border: 1.5px solid var(--status-warning);
  border-radius: 6px;
  font-weight: 700;
  font-size: 0.8125rem;
}
/* Owner (2026-08-15, day-of): "put it to the left, make it smaller" - a
   leading control beside the act title (row is now flex, button inserted as
   the row's first child in av.js), visually SMALL so it stops competing
   with the title, but the actual tap target stays generous via the ::before
   hit-expansion below (dark room, one-handed phone use, stage pressure).
   Still visually unmistakable from a passive row click: filled
   warning-colored circle with a solid border, distinct from any other row
   control (readiness badge is a pill, preview is a plain ▶ glyph). */
.row:has(.av-set-live-btn) {
  display: flex;
  align-items: center;
  gap: 0.5rem;
}
.row:has(.av-set-live-btn) .row-title {
  flex: 1;
  min-width: 0;
}
.av-set-live-btn {
  flex: 0 0 auto;
  position: relative;
  width: 22px;
  height: 22px;
  min-width: 22px;
  min-height: 22px;
  margin: 0;
  padding: 0;
  background: transparent;
  color: var(--status-warning);
  border: 1.5px solid var(--status-warning);
  border-radius: 50%;
  font-weight: 700;
  font-size: 0.6875rem;
  line-height: 1;
  display: inline-flex;
  align-items: center;
  justify-content: center;
}
/* Hit-area expansion: visible glyph is 22px, but the clickable region (this
   pseudo-element, part of the button's own box for event purposes) is
   centered on it and >=44px square - "small to look at, not small to hit". */
.av-set-live-btn::before {
  content: "";
  position: absolute;
  inset: -11px;
}
.av-set-live-btn:active {
  background: var(--status-warning);
  color: var(--bg-primary);
}
/* Phase 8: honest per-pad readiness (what_to_build item 5) - a pad the
   device hasn't actually cached yet is visibly red and disabled, never a
   silently-dead button discovered mid-cue. "unknown" (pre-first-check) reads
   the same as the neutral default so there's no flash of red before the
   real check resolves. */
.soundboard-pad[data-readiness="red"] {
  border-color: var(--status-error);
  box-shadow: inset 0 0 0 2px var(--status-error);
  opacity: 0.6;
}
.soundboard-pad[data-readiness="unavailable"] {
  opacity: 0.4;
}
.soundboard-pad:disabled {
  cursor: not-allowed;
}
.soundboard-empty {
  grid-column: 1 / -1;
  color: var(--text-muted);
  font-size: 0.875rem;
  text-align: center;
  align-self: center;
}

/* AV per-row local-preview play + cache-readiness badge (07-06, LIVE-08) */
.av-readiness-badge {
  margin-left: 0.5rem;
  padding: 0.125rem 0.5rem;
  border-radius: 999px;
  font-size: 0.6875rem;
  font-weight: 600;
  background: var(--bg-tertiary);
  color: var(--text-muted);
}
.av-readiness-badge[data-readiness="green"] {
  background: var(--status-good);
  color: var(--bg-primary);
}
.av-readiness-badge[data-readiness="red"] {
  background: var(--status-error);
  color: var(--bg-primary);
}
.av-preview-play-btn {
  min-width: 44px;
  min-height: 44px;
  margin-left: 0.5rem;
  background: transparent;
  color: var(--brand-primary);
  border: 1px solid var(--brand-primary);
  border-radius: 6px;
}

/* Participant/volunteer own-row highlight + "on in 2" pill (07-07,
   07-CONTEXT.md Decision 10) - persistent, at all times once assignments
   are known, not just when "on in 2" fires. */
.row.own-row {
  border-left: 4px solid var(--brand-primary);
  background: var(--bg-tertiary);
}

#on-in-2-pill {
  position: sticky;
  top: 0;
  z-index: 5;
  margin: 0 1rem;
  padding: 0.375rem 0.75rem;
  border-radius: 999px;
  background: var(--brand-primary);
  color: var(--bg-primary);
  font-weight: 600;
  font-size: 0.875rem;
  text-align: center;
}
#on-in-2-pill[hidden] {
  display: none;
}

/* Volunteer read-only assignments panel */
#volunteer-assignments-panel {
  padding: 1rem;
}
#volunteer-assignments-panel h2 {
  font-size: 1.25rem;
}
#volunteer-assignments-list {
  list-style: none;
  margin: 0;
  padding: 0;
}
#volunteer-assignments-list li {
  padding: 0.5rem 0;
  border-bottom: 1px solid var(--border-color);
}
.volunteer-note {
  margin-top: 1rem;
  color: var(--text-muted);
  font-size: 0.8125rem;
}

@media (max-width: 899px) {
  body[data-role="av"] {
    display: block;
    height: auto;
  }
  body[data-role="av"] #timeline {
    padding-top: 0;
    max-height: 50vh;
  }
  #av-right-column {
    padding-top: 0;
    padding-bottom: calc(1rem + var(--bottom-chrome-h, 0px));
  }
  #av-cue-panel {
    padding: 1rem;
  }
}

/* Admin item detail panel - opened by clicking any timeline row (admin.js).
   Reuses the same fixed-overlay pattern as #host-script-overlay. */
#admin-detail-overlay {
  position: fixed;
  inset: 0;
  z-index: 40;
  display: flex;
  flex-direction: column;
  overflow-y: auto;
  /* Same reason as #host-script-overlay above - reserve room below the
     now-topmost soundboard/mini-player so their fixed footer doesn't
     hide this panel's own bottom content/buttons. */
  padding: 0 1.5rem calc(2rem + var(--bottom-chrome-h, 0px));
  background: var(--bg-primary);
}
#admin-detail-close {
  align-self: flex-start;
  min-height: 44px;
  margin: 0.25rem 0 0.5rem -0.5rem;
  padding: 0.75rem 1rem;
  background: transparent;
  color: var(--text-primary);
  border: none;
  font-size: 1.125rem;
  font-weight: 600;
}
#admin-detail-title {
  margin: 0 0 0.25rem;
  font-size: 1.5rem;
}
#admin-detail-meta {
  margin: 0 0 1rem;
  color: var(--text-secondary);
  font-size: 0.9375rem;
}
#admin-detail-notes {
  padding: 0.75rem;
  margin-bottom: 1rem;
  background: var(--bg-tertiary);
  border-radius: 8px;
  font-size: 0.9375rem;
}
#admin-detail-overlay section {
  margin-bottom: 1.5rem;
}
#admin-detail-overlay h3 {
  margin: 0 0 0.5rem;
  font-size: 1rem;
  color: var(--text-secondary);
}
.detail-empty {
  color: var(--text-muted);
  font-size: 0.9375rem;
}
.detail-people {
  list-style: none;
  margin: 0;
  padding: 0;
}
.detail-people li {
  padding: 0.375rem 0;
  border-bottom: 1px solid var(--border-color);
}
#admin-detail-play {
  min-height: 44px;
  min-width: 96px;
  padding: 0.5rem 1.25rem;
  background: var(--brand-primary);
  color: var(--bg-primary);
  border: none;
  border-radius: 8px;
  font-weight: 700;
}
#admin-detail-play:disabled {
  background: var(--bg-tertiary);
  color: var(--text-muted);
  border: 2px dashed var(--border-color);
}
#admin-detail-script-body {
  white-space: pre-wrap;
  font-size: 1.125rem;
  line-height: 1.5;
  padding: 1rem;
  background: var(--bg-secondary);
  border-radius: 8px;
  cursor: text;
}
#admin-detail-script-body:hover {
  box-shadow: inset 0 0 0 1.5px var(--brand-primary);
}
#admin-detail-script-body[data-empty="true"] {
  color: var(--text-muted);
  font-style: italic;
}
#admin-script-textarea {
  display: block;
  width: 100%;
  min-height: 12rem;
  padding: 1rem;
  background: var(--bg-secondary);
  color: var(--text-primary);
  border: 1.5px solid var(--brand-primary);
  border-radius: 8px;
  font-size: 1.125rem;
  line-height: 1.5;
  font-family: inherit;
  white-space: pre-wrap;
}
#admin-script-actions {
  display: flex;
  gap: 0.5rem;
  margin-top: 0.5rem;
}
#admin-script-actions button {
  min-height: 44px;
  padding: 0.5rem 1.25rem;
  border-radius: 6px;
  font-weight: 700;
}
#admin-script-actions button:first-child {
  background: var(--brand-primary);
  color: var(--bg-primary);
  border: none;
}
#admin-script-actions button:last-child {
  background: transparent;
  color: var(--text-secondary);
  border: 1px solid var(--border-color);
}
#admin-script-status {
  margin-top: 0.5rem;
  font-size: 0.875rem;
  font-weight: 600;
}
#admin-script-status[data-kind="saving"] {
  color: var(--text-secondary);
}
#admin-script-status[data-kind="saved"] {
  color: var(--status-good);
}
#admin-script-status[data-kind="failed"] {
  color: var(--status-error);
}

/* Owner (2026-08-15, day-of, "a mess"): DOM/paint order put show-pack and
   the 36-code invite panel ABOVE the program list on admin at every width,
   not just desktop - "the show comes first" (mobile and desktop alike).
   Flex-column + explicit order re-sequences visually without touching the
   DOM the other admin.js/av.js/app.js modules already query by id. The
   min-width:900px block further down switches this to a real grid two-pane
   for desktop, overriding display: flex wins that media query. */
body[data-role="admin"] {
  display: flex;
  flex-direction: column;
}
body[data-role="admin"] #app-header { order: -100; }
body[data-role="admin"] #offline-banner { order: -90; }
body[data-role="admin"] #standalone-warning { order: -80; }
body[data-role="admin"] #session-recovery { order: -70; }
body[data-role="admin"] #drift-strip { order: 0; }
body[data-role="admin"] #timeline { order: 1; }
body[data-role="admin"] #jump-to-now { order: 2; }
body[data-role="admin"] #admin-bar { order: 3; }
body[data-role="admin"] #admin-detail-overlay { order: 4; }
body[data-role="admin"] #av-right-column { order: 9; }
body[data-role="admin"] #showpack-panel { order: 10; }
body[data-role="admin"] #admin-invite-panel { order: 11; }

/* Owner: the read-only "Your assignments" panel is volunteer-specific dead
   weight on the admin screen ("This list is read-only - there's no way to
   check items off in the app yet." means nothing to the person running the
   show). admin.js still mounts volunteer.js (07-CONTEXT.md's "admin is a
   strict superset" - the shared own-row highlight logic stays wired), this
   only hides the resulting panel's admin-facing DOM node. */
body[data-role="admin"] #volunteer-assignments-panel {
  display: none;
}

/* Admin superset panels (host script/AV cue are the SAME modules those
   roles use, just mounted for admin too - app.js). The soundboard is its
   own fixed footer (styled above) for every role that gets it, admin
   included - not part of this in-flow stack. */
body[data-role="admin"] #av-cue-panel,
body[data-role="admin"] #showpack-panel {
  padding: 1rem;
  margin: 0 1rem 1rem;
  background: var(--bg-secondary);
  border: 1px solid var(--border-color);
  border-radius: 8px;
}
/* Live-prod audit fix (2026-08-15): #av-right-column reserves
   --bottom-chrome-h (soundboard+miniplayer+adminbar height, av.js) as its
   own padding-bottom for AV's single-scroll-region grid layout, where body
   itself never scrolls (height:100vh). Admin has no such grid - body is the
   one scroll container and ALREADY reserves --bottom-chrome-h at the body
   level (rule near the top of this file). Admin was paying for the same
   reserved space twice, opening a ~150px dead gap at the bottom of every
   stacked admin panel (invite/cue/showpack/volunteer) for no reason. */
body[data-role="admin"] #av-right-column {
  padding-bottom: 1rem;
}
/* Live-prod audit fix: 36 join codes rendered as one unbounded list pushed
   everything else on the page (program list, current/next act, show-pack
   download) 1900px+ below the fold - the single biggest contributor to the
   owner's "enormous dead space / unstyled stretched list" report. Same
   compact-card-with-internal-scroll treatment used for #not-show-ready-list
   and every other long list on this page. */
#invite-code-list {
  max-height: 22rem;
  overflow-y: auto;
}

/* Not-show-ready readiness list (app.js renderNotShowReady) - deliberately
   prominent, not buried, per the owner's "who still needs media before
   doors" ask. */
#not-show-ready-list {
  margin-top: 0.75rem;
}
#not-show-ready-list:empty {
  display: none;
}
#not-show-ready-list h3 {
  margin: 0 0 0.375rem;
  font-size: 0.9375rem;
  color: var(--status-warning);
}
#not-show-ready-list ul {
  list-style: none;
  margin: 0;
  padding: 0;
}
#not-show-ready-list li {
  padding: 0.25rem 0;
  font-size: 0.875rem;
  color: var(--text-secondary);
  border-bottom: 1px solid var(--border-color);
}

/* Admin invite-people panel (admin-invite.js) */
#admin-invite-panel {
  margin: 0 1rem 1.5rem;
  padding: 1rem;
  background: var(--bg-secondary);
  border: 1px solid var(--border-color);
  border-radius: 8px;
}
#admin-invite-panel[hidden] {
  display: none;
}
#invite-disclosure summary {
  cursor: pointer;
  font-weight: 700;
  font-size: 1.0625rem;
  padding: 0.25rem 0;
  list-style: none;
}
#invite-disclosure summary::-webkit-details-marker {
  display: none;
}
#invite-disclosure summary::before {
  content: "\25B8 ";
  display: inline-block;
  transition: transform 0.15s ease;
}
#invite-disclosure[open] summary::before {
  transform: rotate(90deg);
}
#invite-disclosure #invite-form {
  margin-top: 0.75rem;
}
#invite-form {
  display: flex;
  flex-direction: column;
  gap: 0.75rem;
}
#invite-form label {
  display: flex;
  flex-direction: column;
  gap: 0.25rem;
  font-size: 0.875rem;
  color: var(--text-secondary);
}
#invite-form select,
#invite-form input {
  min-height: 44px;
  padding: 0.5rem;
  background: var(--bg-tertiary);
  color: var(--text-primary);
  border: 1px solid var(--border-color);
  border-radius: 6px;
  font-size: 1rem;
}
#invite-mint-btn {
  min-height: 44px;
  padding: 0.5rem 1rem;
  background: var(--brand-primary);
  color: var(--bg-primary);
  border: none;
  border-radius: 6px;
  font-weight: 700;
}
#invite-result {
  margin-top: 1rem;
}
#invite-result:empty {
  margin-top: 0;
}
#invite-result-link-row {
  display: flex;
  gap: 0.5rem;
  margin: 0.5rem 0 1rem;
}
#invite-result-url {
  flex: 1;
  min-height: 44px;
  padding: 0.5rem;
  background: var(--bg-tertiary);
  color: var(--text-primary);
  border: 1px solid var(--border-color);
  border-radius: 6px;
  font-size: 0.875rem;
}
#invite-copy-btn {
  min-height: 44px;
  padding: 0.5rem 1rem;
  background: var(--brand-primary);
  color: var(--bg-primary);
  border: none;
  border-radius: 6px;
  font-weight: 600;
}
#invite-qr {
  display: inline-block;
  padding: 0.5rem;
  background: #fff;
  border-radius: 8px;
  line-height: 0;
}
#invite-qr svg {
  display: block;
  width: 160px;
  height: 160px;
}
.invite-error {
  color: var(--status-error);
}
#invite-code-list {
  list-style: none;
  margin: 0.5rem 0 0;
  padding: 0;
}
#invite-code-list li {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  padding: 0.5rem 0;
  border-bottom: 1px solid var(--border-color);
  font-size: 0.875rem;
}
.invite-code-who {
  flex: 1;
}
.invite-code-status {
  color: var(--text-muted);
  font-size: 0.75rem;
  text-transform: uppercase;
}
li[data-revoked="true"] .invite-code-who {
  text-decoration: line-through;
  color: var(--text-muted);
}
.invite-revoke-btn {
  min-height: 36px;
  padding: 0.25rem 0.75rem;
  background: transparent;
  color: var(--status-error);
  border: 1px solid var(--status-error);
  border-radius: 6px;
  font-size: 0.8125rem;
}

/* Desktop-width layout (owner-reported: no breathing room, admin-bar
   crammed bottom-left, jump input overlapping content, at 1440px). Content
   stays readable-width and centered; layout below stays exactly as-is on
   phones (no change under this breakpoint). */
@media (min-width: 900px) {
  body:not([data-role="av"]) #drift-strip,
  body:not([data-role="av"]) #timeline,
  body:not([data-role="av"]) #showpack-panel,
  #admin-invite-panel,
  #session-recovery {
    max-width: 720px;
    margin-left: auto;
    margin-right: auto;
  }
  body:not([data-role="av"]) #admin-bar {
    justify-content: center;
    max-width: 720px;
    margin: 0 auto;
    padding: 1rem;
  }
  #admin-jump-label {
    position: relative;
    display: inline-flex;
    align-items: center;
    gap: 0.5rem;
  }
  /* Owner spec: detail goes in a SIDEBAR on tablet/desktop (mobile keeps
     the inline full-panel behavior below) - same markup, CSS-driven. */
  body[data-role="admin"] #admin-detail-overlay {
    position: fixed;
    inset: var(--header-h, 0px) 0 0 auto;
    left: auto;
    width: min(420px, 40vw);
    border-left: 1px solid var(--border-color);
    box-shadow: -4px 0 16px rgba(0, 0, 0, 0.4);
  }
}

/* Owner (2026-08-15, day-of, "a mess"): the admin desktop page was one long
   single-flow column - the ~36-code invite panel and show-pack panel sat
   ABOVE the program list in DOM order, so the operator had to scroll past a
   full screen of setup tooling to reach the show. Real two-pane restore:
   program list is a true left sidebar, item detail a real right pane beside
   it (not just a fixed overlay competing with a centered-column timeline),
   and secondary/setup tooling (invite, show-pack) moves into that same right
   column BELOW the detail pane instead of floating above everything at the
   top of the page. Mobile (<900px) is untouched - same inline stacked
   behavor as before. */
@media (min-width: 900px) {
  /* grid-auto-flow: dense with EVERY direct child given an explicit
     grid-row (not just some) - CSS grid auto-placement only reserves the
     exact cell an explicit item asks for, so any child left with an "auto"
     row has to independently hunt for its own free cell and can land
     somewhere unrelated to its visual neighbors once other cells are
     reserved (caught in real-browser verification: the auto-placed
     elements were scattering across the row 5+ once explicit right-column
     cells were reserved, hiding the timeline). Every real body child is
     listed below by name so nothing is left to auto-placement guesswork -
     #player/#cue-player-staged (invisible audio elements) and any future
     admin-only panel not in this list still render (grid never drops an
     item, worst case an unstyled one lands in row1 col1), just not in a
     guaranteed slot. */
  body[data-role="admin"] {
    display: grid;
    grid-template-columns: 380px minmax(0, 1fr);
    grid-auto-rows: min-content;
    align-items: start;
    column-gap: 1.5rem;
    row-gap: 0;
  }
  body[data-role="admin"] #app-header { grid-row: 1; grid-column: 1 / -1; }
  body[data-role="admin"] #offline-banner { grid-row: 2; grid-column: 1 / -1; }
  body[data-role="admin"] #standalone-warning { grid-row: 3; grid-column: 1 / -1; }
  body[data-role="admin"] #session-recovery { grid-row: 4; grid-column: 1 / -1; }
  /* Left column: the show itself - un-centers the shared max-width:720px
     centering rule above, this is now a sidebar, not a page column. */
  body[data-role="admin"] #drift-strip { grid-row: 5; grid-column: 1; max-width: none; margin-left: 0; margin-right: 0; }
  body[data-role="admin"] #timeline {
    grid-row: 6;
    grid-column: 1;
    max-width: none;
    margin-left: 0;
    margin-right: 0;
    max-height: calc(100vh - var(--header-h, 0px) - 2rem);
    overflow-y: auto;
  }
  body[data-role="admin"] #jump-to-now { grid-row: 7; grid-column: 1; }
  body[data-role="admin"] #admin-bar { grid-row: 8; grid-column: 1; max-width: none; margin-left: 0; margin-right: 0; }
  /* Right column: item detail first (what the operator opens most), the AV
     cue panel (admin is a strict superset - av.js mounts it here too), then
     setup tooling (show-pack, invite) below - never above the show. */
  body[data-role="admin"] #admin-detail-overlay {
    grid-row: 5;
    grid-column: 2;
    position: static;
    width: auto;
    max-width: 640px;
    margin: 0 1rem 1rem 0;
    border-left: 1px solid var(--border-color);
    box-shadow: none;
    max-height: calc(100vh - var(--header-h, 0px) - 2rem);
    overflow-y: auto;
  }
  body[data-role="admin"] #av-right-column { grid-row: 6; grid-column: 2; position: static; height: auto; }
  body[data-role="admin"] #showpack-panel { grid-row: 7; grid-column: 2; max-width: 640px; margin: 0 1rem 1rem 0; }
  body[data-role="admin"] #admin-invite-panel { grid-row: 8; grid-column: 2; max-width: 640px; margin: 0 1rem 1rem 0; }
  /* Owner: "tame the home-screen banner" - full-width yellow block was
     dominating the top of the page. Compact one-liner, still dismissible
     (existing button), never pushes the show content down further than a
     single slim row. */
  body[data-role="admin"] #standalone-warning {
    padding: 0.375rem 1rem;
    font-size: 0.8125rem;
  }
  body[data-role="admin"] #standalone-warning button {
    min-height: 32px;
    padding: 0.125rem 0.625rem;
  }
}

/* Wake Lock failure/unsupported banner (D-13). UI audit fix: this was
   `position:fixed`, which directly contradicted av.js's own showWakeLockBanner
   comment ("insert as a normal, static-flow element... PUSH content down
   instead of covering it") - the JS inserted it in-flow but this rule then
   pulled it out of flow again and overlaid it on top of whatever was
   underneath (the sidebar's program list on AV desktop). Normal flow here;
   AV's grid layout gets its own explicit placement + margin-top below. */
#wakelock-banner {
  z-index: 30;
  padding: 0.75rem 1rem;
  background: var(--status-warning);
  color: var(--bg-primary);
  font-weight: 600;
  display: flex;
  justify-content: space-between;
  align-items: center;
  gap: 1rem;
}
#wakelock-banner button {
  min-height: 44px;
  padding: 0.25rem 0.75rem;
  background: var(--bg-primary);
  color: var(--text-primary);
  border: none;
  border-radius: 6px;
}

/* Standalone-install warning banner (D-22, app.js's warnIfNotStandalone).
   UI audit fix: this had NO styling at all - it rendered as an unstyled div
   with a bare browser-default button, and for the same implicit-grid-row
   reason as #wakelock-banner/#showpack-panel, floated pinned to the bottom
   of the AV viewport overlapping the sidebar's last visible rows. Styled to
   match the wake-lock banner (same role: dismissible amber warning), with
   AV grid placement above. */
#standalone-warning {
  padding: 0.75rem 1rem;
  background: var(--status-warning);
  color: var(--bg-primary);
  font-weight: 600;
  display: flex;
  justify-content: space-between;
  align-items: center;
  gap: 1rem;
  z-index: 30;
}
#standalone-warning[hidden] {
  display: none;
}
#standalone-warning button {
  min-height: 44px;
  padding: 0.25rem 0.75rem;
  background: var(--bg-primary);
  color: var(--text-primary);
  border: none;
  border-radius: 6px;
}
