.pagination-container {
  display: flex;
  justify-content: center;
}


.chuds-amount {
  /* border: 0px; */
  padding: 0;
  margin: 0;
  font-size: 2em;
  text-align: center;
  max-width: 80px;
}

ul.timeline {
  list-style-type: none;
  position: relative;
}
ul.timeline:before {
  content: ' ';
  background: #d4d9df;
  display: inline-block;
  position: absolute;
  left: 29px;
  width: 2px;
  height: 100%;
  z-index: 400;
}
ul.timeline > li {
  margin: 20px 0;
  padding-left: 20px;
}
ul.timeline > li:before {
  content: ' ';
  background: white;
  display: inline-block;
  position: absolute;
  border-radius: 50%;
  border: 3px solid #22c0e8;
  left: 20px;
  width: 20px;
  height: 20px;
  z-index: 400;
}

.icon-text-white {
  /* 
    https://isotropic.co/tool/hex-color-to-css-filter/
  */
  
  filter: invert(97%) sepia(98%) saturate(64%) hue-rotate(271deg) brightness(115%) contrast(100%);
}

.fade-out {
  opacity: 0;
  transition: opacity 0.5s ease-in-out;
}

.fade-highlight {
  transition: background-color 2s ease-out;
  background-color: transparent !important;
}

.pagy {
  display: flex;
  font-family: sans-serif;
  font-size: 0.875rem;
  line-height: 1.25rem;
  font-weight: 600;
  color: #6b7280;
}
.pagy > :not([hidden]) ~ :not([hidden]) {
  --space-reverse: 0;
  margin-right: calc(0.25rem * var(--space-reverse));
  margin-left: calc(0.25rem * calc(1 - var(--space-reverse)));
}
.pagy a:not(.gap) {
  display: block;
  text-decoration: none;
  border-radius: 0.5rem;
  background-color: #e5e7eb;
  padding: 0.25rem 0.75rem;
  color: inherit;
}
.pagy a:not(.gap):hover {
  background-color: #d1d5db;
}
.pagy a:not(.gap):not([href]) { /* disabled links */
  cursor: default;
  background-color: #f3f4f6;
  color: #d1d5db;
}
.pagy a:not(.gap).current {
  background-color: #9ca3af;
  color: white;
}
.pagy label {
  white-space: nowrap;
  display: inline-block;
  border-radius: 0.5rem;
  background-color: #e5e7eb;
  padding: 0.125rem 0.75rem;
}
.pagy label input {
  line-height: 1.5rem;
  border-radius: 0.375rem;
  border-style: none;
  background-color: #f3f4f6;
}

/* Row striping stays as you have */


/* Qty pills for line items */
.qty-pill {
  display: inline-flex; align-items: center; justify-content: center;
  min-width: 38px; height: 28px; border-radius: 8px;
  font-weight: 700; color: #fff; padding: 0 .5rem;
}

/* ===== performers/pay page only ===== */
#performers-pay {
  background-image: url("/assets/GrossProphets-StoryBKG-Clouds-9x16-39928e9f.png");
  background-size: cover;
  background-position: center top;
  background-repeat: no-repeat;
}

/* Hero */
#performers-pay .pay-hero .hero-inner {
  max-width: 560px;
}
#performers-pay .pay-hero .hero-title {
  color: #fff;
  text-shadow: 0 1px 2px rgba(0,0,0,.35);
  font-weight: 800;
}
#performers-pay .pay-hero .hero-subtitle {
  color: rgba(255,255,255,.9);
  text-shadow: 0 1px 2px rgba(0,0,0,.25);
  margin-bottom: 0.75rem;
}
#performers-pay .pay-hero .hero-mascot {
  filter: drop-shadow(0 2px 6px rgba(0,0,0,.35));
}

/* Remaining chuds pill */
#performers-pay .balance-pill {
  border-radius: 999px;
  background: rgba(255, 255, 255, 0.25);
  backdrop-filter: blur(6px);
  border: 1px solid rgba(255, 255, 255, 0.55);
  color: #fff;
  min-width: 70px;  /* keep pill shape */
}

/* Performer cards: soft glass on white so bg peeks through */
#performers-pay .performer-card {
  background: rgba(255, 255, 255, 0.78);              /* <= transparency */
  border: 1px solid rgba(255, 255, 255, 0.6);
  border-radius: 1rem;
  backdrop-filter: saturate(140%) blur(4px);           /* subtle glass */
  -webkit-backdrop-filter: saturate(140%) blur(4px);   /* iOS Safari */
  box-shadow:
    0 6px 16px rgba(0, 0, 0, 0.12),
    0 1px 0 rgba(255, 255, 255, 0.6) inset !important;            /* faint top shine */
}

/* Two-line stacked name style (subtle stylization like the mock) */
#performers-pay .name-stack .name-top { letter-spacing: .04em; }
#performers-pay .name-stack .name-bottom { letter-spacing: .06em; font-size: 1.05rem; }

/* Quantity control */
/* The overall pill */
#performers-pay .qty-pill {
  display: inline-flex;
  align-items: center;
  gap: .25rem;
  padding: 1.25rem .5rem;
  border-radius: 999px;
  background: rgba(255, 255, 255, 0.65);
  border: 1px solid rgba(255, 255, 255, 0.75);
  box-shadow:
    inset 0 1px 2px rgba(0,0,0,.06),
    0 2px 6px rgba(0,0,0,.08);
}

/* Kill outlines/borders/backgrounds on +/– so it feels like one pill */
#performers-pay .qty-pill .qty-btn {
  border: 0 !important;
  background: transparent !important;
  box-shadow: none !important;
  padding: .25rem .5rem;          /* bigger tap target */
  line-height: 1;
}
#performers-pay .qty-pill .qty-btn:focus,
#performers-pay .qty-pill .qty-btn:active {
  outline: none !important;
  box-shadow: none !important;
  background: transparent !important;
}

/* Icon sizing inside the pill */
#performers-pay .qty-pill .qty-btn svg {
  width: 22px;
  height: 22px;
}

/* The number field melts into the pill */
#performers-pay .qty-pill .qty-input {
  width: 2.5rem;
  text-align: center;
  border: 0;
  background: transparent;
  box-shadow: none !important;
  font-weight: 800;
  font-size: 1.25rem;
  padding: 0;
}
#performers-pay .qty-pill .qty-input:focus {
  outline: none;
  box-shadow: none;
}

/* Optional: tighter look—remove any residual Bootstrap borders */
#performers-pay .qty-pill .btn-warning,
#performers-pay .qty-pill .btn-outline-secondary {
  border: 0 !important;
  background: transparent !important;
}


#performers-pay .qty-input {
  width: 2.5rem;
  background: transparent;
  font-weight: 700;
  font-size: 1.25rem;
  padding: 0;
  height: 2rem;
  line-height: 2rem;
}
#performers-pay .qty-input:focus {
  box-shadow: none;
}
#performers-pay .qty-btn {
  width: 2rem;
  height: 2rem;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  padding: 0;
}

/* Sticky CTA
#performers-pay .send-chuds-cta {
  background: linear-gradient(to top, rgba(255,255,255,.9), rgba(255,255,255,0));
} */

.transparent-glass {
  position: relative;
  backdrop-filter: blur(2px) saturate(180%);
  border: 1px solid rgba(255, 255, 255, 0.8);
  border-radius: 2rem;
  box-shadow: 0 8px 32px rgba(31, 38, 135, 0.2), 
              inset 0 4px 20px rgba(255, 255, 255, 0.3);

}
.transparent-glass::after {
  content: '';
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  background: rgba(255, 255, 255, 0.1);
  border-radius: 2rem;
  backdrop-filter: blur(1px);
  box-shadow: inset -10px -8px 0px -11px rgba(255, 255, 255, 1),
              inset 0px -9px 0px -8px rgba(255, 255, 255, 1);
  opacity: 0.6;
  z-index: -1;
  filter: blur(1px) drop-shadow(10px 4px 6px black) brightness(115%);
}


.glass {
  position: relative;
  background: rgba(255, 255, 255, 0.15);
  backdrop-filter: blur(2px) saturate(180%);
  border: 1px solid rgba(255, 255, 255, 0.8);
  border-radius: 2rem;
  box-shadow: 0 8px 32px rgba(31, 38, 135, 0.2), 
              inset 0 4px 20px rgba(255, 255, 255, 0.3);
}

.glass::after {
  content: '';
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  background: rgba(255, 255, 255, 0.1);
  border-radius: 2rem;
  backdrop-filter: blur(1px);
  box-shadow: inset -10px -8px 0px -11px rgba(255, 255, 255, 1),
              inset 0px -9px 0px -8px rgba(255, 255, 255, 1);
  opacity: 0.6;
  z-index: -1;
  filter: blur(1px) drop-shadow(10px 4px 6px black) brightness(115%);
}
/* The dashboard's tabs. Bootstrap 5.0.2 has no .nav-underline — it arrived in
   5.3 — and the tabs here are plain links rather than a JS component, because
   which screen the operator is on has to survive a redirect and a reload. */
.nav-underline .nav-link {
  border-bottom: 2px solid transparent;
  color: var(--bs-gray-700, #495057);
  padding-bottom: 0.5rem;
}

.nav-underline .nav-link:hover,
.nav-underline .nav-link:focus {
  border-bottom-color: var(--bs-gray-400, #ced4da);
}

.nav-underline .nav-link.active {
  border-bottom-color: var(--bs-primary, #0d6efd);
  color: var(--bs-primary, #0d6efd);
  font-weight: 600;
}

.dashboard-tabs .nav-link.active {
  border-bottom-width: 3px;
}

/* While the next screen is on its way. A tab press that looks like nothing
   happened gets pressed again. */
.dashboard-tabs.is-loading .nav-link.is-target::after {
  content: "";
  display: inline-block;
  width: 0.75em;
  height: 0.75em;
  margin-left: 0.4em;
  vertical-align: -0.1em;
  border: 2px solid currentColor;
  border-right-color: transparent;
  border-radius: 50%;
  animation: gp-spin 0.6s linear infinite;
}

@keyframes gp-spin {
  to { transform: rotate(360deg); }
}

/* The busy state of a segmented control. The spinner sits BESIDE the label
   rather than replacing it: the label vanishing for a moment reads as the
   button having lost its mind. */
.segmented__btn.is-busy::after {
  content: "";
  display: inline-block;
  width: 0.75em;
  height: 0.75em;
  margin-left: 0.4em;
  vertical-align: -0.1em;
  border: 2px solid currentColor;
  border-right-color: transparent;
  border-radius: 50%;
  animation: gp-spin 0.6s linear infinite;
}

@media (prefers-reduced-motion: reduce) {
  .dashboard-tabs.is-loading .nav-link.is-target::after,
  .segmented__btn.is-busy::after { animation: none; }
}

/* ---------------------------------------------------------------------------
   The dashboard, on a phone, in a booth. Bootstrap 5.0.2 is what this app has;
   everything below is what 5.0.2 does not carry.
   --------------------------------------------------------------------------- */

/* Flash as an overlay rather than a block in the flow. Fixed, so nothing under
   it moves when a cue lands mid-show. */
.admin-toasts {
  position: fixed;
  bottom: 0.75rem;
  left: 50%;
  transform: translateX(-50%);
  z-index: 1080;
  width: min(36rem, calc(100vw - 1.5rem));
}

.admin-toasts .alert {
  margin-bottom: 0.5rem;
}

/* The tabs scroll rather than wrap: a second row of tabs moves every control on
   the screen down, and the operator is reading this one-handed. Edge to edge on
   a phone, so the scroll is visibly a scroll. */
.dashboard-tabs {
  flex-wrap: nowrap;
  overflow-x: auto;
  overflow-y: hidden;
  -webkit-overflow-scrolling: touch;
  scrollbar-width: none;
  scroll-behavior: smooth;
  /* The muted rule the active underline sits on. The underline is thicker and
     sits over this, so the selected tab reads as a mark on the line rather than
     a break in it. */
  border-bottom: 1px solid var(--bs-gray-300, #dee2e6);
  /* Soft at both ends rather than a hard cut, so a tab half out of view reads
     as "there is more" instead of as a clipped word. */
  -webkit-mask-image: linear-gradient(to right, transparent 0, #000 1.25rem,
                                      #000 calc(100% - 1.25rem), transparent 100%);
  mask-image: linear-gradient(to right, transparent 0, #000 1.25rem,
                              #000 calc(100% - 1.25rem), transparent 100%);
}

.dashboard-tabs::-webkit-scrollbar {
  display: none;
}

.dashboard-tabs .nav-link {
  white-space: nowrap;
  /* Over the nav's own rule, not above it. */
  margin-bottom: -1px;
}

/* The show header: what is true on every tab. */
.show-header .show-header__qr {
  line-height: 1;
  color: var(--bs-gray-600, #6c757d);
  text-decoration: none;
}

.show-header .show-header__qr:hover {
  color: var(--bs-primary, #0d6efd);
}

.show-header__qr svg {
  width: 1rem;
  height: 1rem;
}

/* Three tiles, equal width, each a link to the screen that changes it. */
.show-tiles {
  margin-left: -0.5rem;
  margin-right: -0.5rem;
}

.show-tile {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 0.125rem;
  height: 100%;
  padding: 0.5rem 0.25rem;
  border-top: 1px solid var(--bs-gray-200, #e9ecef);
  color: inherit;
  text-align: center;
  text-decoration: none;
}

/* The header card's top rule separates the tiles from the card above it. A
   card that already opens with its own top border — the Participation tab's
   summary card — does not need a second one directly under it. */
.show-tile--flush {
  border-top: 0;
}

.show-tile:hover,
.show-tile:focus {
  background-color: var(--bs-gray-100, #f8f9fa);
  color: inherit;
}

.show-tile__label {
  font-size: 0.75rem;
  text-transform: uppercase;
  letter-spacing: 0.03em;
  color: var(--bs-gray-600, #6c757d);
}

.show-tile__value {
  font-size: 1.25rem;
  font-weight: 700;
  line-height: 1.1;
}

.show-tile__value--success { color: var(--bs-success, #198754); }
.show-tile__value--info    { color: var(--bs-info, #0dcaf0); }
.show-tile__value--warning { color: var(--bs-warning, #ffc107); }
.show-tile__value--dark    { color: var(--bs-dark, #212529); }
.show-tile__value--secondary { color: var(--bs-gray-600, #6c757d); }

/* The segmented control. One pill, moved by a custom property the server sets,
   so the browser animates the move and there is no JS to keep in step with the
   state. Each segment stays its own form: the URL a segment posts to is the URL
   the QLab cue fires. */
.segmented {
  position: relative;
  display: flex;
  padding: 0.25rem;
  background-color: var(--bs-gray-200, #e9ecef);
  border-radius: 0.5rem;
}

.segmented__pill {
  position: absolute;
  top: 0.25rem;
  bottom: 0.25rem;
  left: 0.25rem;
  width: calc((100% - 0.5rem) / var(--segments));
  border-radius: 0.375rem;
  background-color: var(--bs-secondary, #6c757d);
  box-shadow: 0 1px 2px rgba(0, 0, 0, 0.15);
  transform: translateX(calc(100% * var(--segment-index)));
  transition: transform 0.2s ease-in-out, background-color 0.2s ease-in-out;
}

.segmented__pill--success { background-color: var(--bs-success, #198754); }
.segmented__pill--info    { background-color: var(--bs-info, #0dcaf0); }
.segmented__pill--warning { background-color: var(--bs-warning, #ffc107); }
.segmented__pill--dark    { background-color: var(--bs-dark, #212529); }
.segmented__pill--secondary { background-color: var(--bs-gray-600, #6c757d); }

@media (prefers-reduced-motion: reduce) {
  .segmented__pill { transition: none; }
}

.segmented__form {
  flex: 1 1 0;
  min-width: 0;
}

.segmented__btn {
  position: relative;
  z-index: 1;
  width: 100%;
  padding: 0.375rem 0.5rem;
  border: 0;
  border-radius: 0.375rem;
  background: transparent;
  color: var(--bs-gray-700, #495057);
  font-weight: 500;
  white-space: nowrap;
}

.segmented__btn.is-current {
  color: #fff;
  font-weight: 600;
}

/* Warning is the one state light enough that white text on it fails to read. */
.segmented__pill--warning ~ .segmented__form .segmented__btn.is-current {
  color: var(--bs-dark, #212529);
}

/* A segment the server will not accept. The quiz uses one: a question the room
   has already been shown the answer to cannot go back up, so "open" is offered
   greyed rather than as a button whose only outcome is a refusal. */
.segmented__btn:disabled {
  opacity: 0.4;
  cursor: not-allowed;
  text-decoration: line-through;
}

.segmented.is-pending {
  opacity: 0.6;
  cursor: progress;
}

/* The destructive end of a screen, fenced off. */
.danger-zone {
  border-color: var(--bs-danger, #dc3545);
}

.danger-zone__title {
  font-size: 0.75rem;
  letter-spacing: 0.08em;
}

/* One performer, as a card. The table this replaces ran off the side of a
   phone, and the ID column was the widest thing on it. */
.performer-card {
  padding: 0.75rem;
  border: 1px solid var(--bs-gray-300, #dee2e6);
  border-radius: 0.5rem;
  margin-bottom: 0.5rem;
  background-color: #fff;
  transition: opacity 0.15s ease-in-out, border-color 0.15s ease-in-out;
  cursor: pointer;
}

/* Everyone who is not the one being paid. The number about to be typed belongs
   to one person, and that has to be unmistakable at a glance. */
.performer-card.is-muted {
  opacity: 0.4;
}

.performer-card.is-open {
  border-color: var(--bs-primary, #0d6efd);
}

.performer-card__head {
  display: flex;
  align-items: center;
  gap: 0.75rem;
}

.performer-card__avatar {
  flex: 0 0 auto;
  width: 3rem;
  height: 3rem;
  border-radius: 50%;
  object-fit: cover;
}

.performer-card__avatar--initial {
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 1.25rem;
  font-weight: 700;
  text-transform: uppercase;
  color: var(--bs-dark, #212529);
}

.performer-card__who {
  min-width: 0;
  flex: 1 1 auto;
}

.performer-card__name {
  font-size: 1.125rem;
  font-weight: 700;
  line-height: 1.2;
}

.performer-card__meta {
  color: var(--bs-gray-600, #6c757d);
  font-size: 0.875rem;
}

.performer-card__sep {
  margin: 0 0.25rem;
}

.performer-card__chuds {
  flex: 0 0 auto;
  text-align: right;
  line-height: 1.05;
}

.performer-card__chuds-value {
  display: block;
  font-size: 1.75rem;
  font-weight: 700;
}

.performer-card__chuds-label {
  display: block;
  font-size: 0.6875rem;
  text-transform: uppercase;
  letter-spacing: 0.03em;
  color: var(--bs-gray-600, #6c757d);
}

.performer-card__controls {
  margin-top: 0.75rem;
}

/* The modal's scrim. Bootstrap 5.0.2 renders .modal-backdrop at 0.5 opacity;
   over a page that is mostly white cards it reads as barely there, and what is
   behind the QR keeps competing with it. */
.modal-backdrop.show {
  opacity: 0.7;
}

.modal-content {
  box-shadow: 0 1rem 3rem rgba(0, 0, 0, 0.4);
}

.attendee-optin svg {
  width: 0.85em;
  height: 0.85em;
}

/* The drag handle for reordering quiz questions, moments and answers. A
   dedicated cursor and focus ring, rather than an icon font pulled in for one
   glyph — the dots read as a handle on their own. */
.drag-handle {
  cursor: grab;
  display: inline-block;
  padding: 0 0.35em;
  font-size: 1.1rem;
  line-height: 1;
  user-select: none;
}

.drag-handle:active {
  cursor: grabbing;
}

.drag-handle:focus-visible {
  outline: 2px solid var(--bs-primary, #0d6efd);
  outline-offset: 2px;
  border-radius: 0.2rem;
}

/* Wide admin tables, on a phone.
   Below md a table that does not FIT stops being a grid and becomes one card
   per row: every cell on its own line with its column heading beside it. Both
   halves of that come from the stacked-table Stimulus controller — the heading
   text, because CSS cannot read another element's text, and the is-stacked
   class, because CSS cannot ask whether a table overflowed. A narrow table
   keeps its columns: stacking a three-column list of levels would turn short
   rows into three lines each for no gain.

   The alternative was the sideways scroll that .table-responsive gives, which
   hides half of every row behind a gesture nobody makes in a booth. */
@media (max-width: 767.98px) {
  .table-stacked.is-stacked thead {
    /* Not display:none — that takes the headings out of the accessibility tree
       as well, and they are still the names of the values below. */
    position: absolute;
    width: 1px;
    height: 1px;
    overflow: hidden;
    clip: rect(0 0 0 0);
    white-space: nowrap;
  }

  .table-stacked.is-stacked,
  .table-stacked.is-stacked tbody,
  .table-stacked.is-stacked tr,
  .table-stacked.is-stacked td {
    display: block;
    width: auto;
  }

  .table-stacked.is-stacked tr {
    border: 1px solid var(--bs-border-color, #dee2e6);
    border-radius: 0.375rem;
    margin-bottom: 0.75rem;
    padding: 0.25rem 0.75rem;
    background: var(--bs-body-bg, #fff);
  }

  /* Striping is a row-level cue and rows are cards now, so it reads as a card
     colour rather than as a stripe. */
  .table-stacked.is-stacked.table-striped > tbody > tr:nth-of-type(odd) > td {
    background: transparent;
  }

  .table-stacked.is-stacked td {
    border: 0;
    border-bottom: 1px solid var(--bs-border-color-translucent, rgba(0, 0, 0, 0.1));
    padding: 0.5rem 0;
    text-align: left;
  }

  .table-stacked.is-stacked tr > td:last-child {
    border-bottom: 0;
  }

  /* The label, from the controller. A cell with no data-label — an actions
     column, whose heading is deliberately blank — just takes the full width. */
  .table-stacked.is-stacked td[data-label]::before {
    content: attr(data-label);
    display: block;
    font-size: 0.75rem;
    font-weight: 600;
    text-transform: uppercase;
    letter-spacing: 0.02em;
    color: var(--bs-secondary-color, #6c757d);
    margin-bottom: 0.125rem;
  }

  /* A cell holding nothing would otherwise be a label with a blank under it. */
  .table-stacked.is-stacked td:empty {
    display: none;
  }
}

/* A row's menu, inside a table that scrolls.
   .table-responsive scrolls sideways, and an element that scrolls on one axis
   clips on the other too — so a menu opened on the last row is cut off at the
   table's bottom edge. Only while one is open, because the scroll is what
   holds a wide table together on the rows where no menu is showing. Below md
   these tables stack instead, so this is the laptop's case. */
.table-responsive:has(.dropdown-menu.show) {
  overflow: visible;
}

/* A setting's value in the list. Long ones are truncated server-side; this is
   what keeps a value with no spaces in it — a key, a URL, a minified line —
   from pushing the column wider than the screen anyway. */
.setting-preview {
  display: inline-block;
  max-width: 100%;
  overflow-wrap: anywhere;
  font-family: var(--bs-font-monospace, monospace);
  font-size: 0.875rem;
}

/* The quiz card's heading is its own toggle, so the whole width of it is a
   target for a thumb in the dark rather than a chevron at one end. It is a
   button, so Bootstrap's link styling has to be undone to leave the heading
   looking like a heading. */
.quiz-card__toggle {
  color: inherit;
  border: 0;
  background: none;
}

.quiz-card__toggle:hover,
.quiz-card__toggle:focus {
  color: inherit;
}

/* Which way the card is facing. Rotates rather than swapping glyphs, so there
   is one thing to look at and it moves. */
.quiz-card__toggle::after {
  content: "\203A";
  float: right;
  margin-left: 0.5rem;
  transform: rotate(90deg);
  transition: transform 0.15s ease-in-out;
  display: inline-block;
}

.quiz-card__toggle[aria-expanded="false"]::after {
  transform: rotate(0deg);
}

@media (prefers-reduced-motion: reduce) {
  .quiz-card__toggle::after { transition: none; }
}

/* A takeover: the whole phone, with nothing to close it.
   Used by the two moments the show cannot proceed past — a quiz question the
   room is answering, and the "I'm ready" the operator is waiting on. Both stay
   until the operator moves on, which is why there is no close control here to
   style: there isn't one. */
.takeover {
  position: fixed;
  inset: 0;
  z-index: 1055;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 1rem;
  overflow-y: auto;
  overscroll-behavior: contain;
}

/* Participation: the page behind is gone. Being about to go on stage is not
   something to read past, so there is nothing behind the card to look at. */
.takeover--blocking {
  background: var(--bs-dark, #212529);
}

/* The quiz: a big card sitting high, with the page dimmed but still there
   behind it. The room is answering a question, not leaving the site — and the
   card is toward the top so a thumb reaches the answers without stretching. */
.takeover--quiz {
  background: rgba(0, 0, 0, 0.6);
  align-items: flex-start;
  padding-top: 3.5rem;
}

.takeover--quiz .takeover__inner {
  max-width: 36rem;
}

.takeover--quiz .takeover__card {
  padding: 1.5rem;
}

.takeover__inner {
  width: 100%;
  max-width: 32rem;
  /* A long question with many answers scrolls inside the card rather than
     pushing the buttons off a short phone. */
  max-height: 100%;
  overflow-y: auto;
}

.takeover__card {
  background: var(--bs-body-bg, #fff);
  color: var(--bs-body-color, #212529);
  border-radius: 0.75rem;
  padding: 1.25rem;
  box-shadow: 0 1rem 3rem rgba(0, 0, 0, 0.4);
}

/* Participation outranks the quiz when both are up. Being about to go on stage
   is more urgent than a question the whole room is answering, and the operator
   is holding a cue on the first one. */
.takeover--blocking {
  z-index: 1060;
}

/* The Chud Checkpoint takeover.

   1065 clears standby (1060) and the quiz (1055), which is the order Corey
   settled on 2026-09-14: a checkpoint is the most important thing anyone is
   doing for that minute, unless they personally have something to do in the
   show. */
.takeover--checkpoint {
  z-index: 1065;
}

/* The two states that outrank it, made to outrank it.

   Being chosen and being on stage both beat the checkpoint; standby does not.
   But being chosen and standby are one class (.takeover--blocking) on two
   partials, so no z-index can put the checkpoint between them. This does the
   split by presence instead, which also means the checkpoint comes back by
   itself when the higher frame clears — no broadcast, no round trip.

   The first selector is the id the instruction card already sets. The second
   identifies participation/_live by what makes it structurally unique: a modal
   dialog inside the participation frame that is NOT a .takeover, because it is
   styled inline rather than with the class. Neither hook belongs to this
   change, so neither breaks when the other session refactors — and both fail
   visible rather than hidden.

   The floating reminder's reopened confirmation card (_reminder.html.erb)
   sits under `hidden` inside #participation_frame too, every accepted
   attendee, whether tapped open or not — but it renders with a distinct id,
   `participation_instruction_header_reopened`, so the exact-match attribute
   selector below does not see it. That is deliberate (_instruction_card.html.erb),
   not incidental: this rule stays correct without a `.takeover--reopened`
   exclusion because it never keys off the class the reopened copy shares with
   a real takeover, only the id the genuine one owns alone. Verified when the
   sale-toast suppression below got that exclusion (sale-toast-layer branch). */
body:has(#participation_frame [aria-labelledby="participation_instruction_header"]) .takeover--checkpoint,
body:has(#participation_frame [role="dialog"][aria-modal="true"]:not(.takeover)) .takeover--checkpoint {
  display: none;
}

/* Sale toasts. --gp-zIndex-sale-toast (1070, tokens.css) clears the Chud
   Checkpoint (1065) — a sale still shows while one is up — and stays under
   being on stage (1075), the one thing nothing should cover.

   Suppressed outright, not merely out-stacked, while the quiz or any
   participation takeover owns the screen: nobody can buy anything during
   either, so a toast there would only cover a question or a cue. Reuses the
   two hooks the checkpoint suppression above already established —
   .takeover--blocking covers the opt-in ask, being chosen and standby/ready
   in one class; #participation_frame's dialog covers being live on stage,
   which is not a .takeover because it is styled inline (see _live.html.erb)
   — plus .takeover--quiz, which the checkpoint never had to consider because
   the checkpoint already outranks it. These are broadcast, not queued: a
   suppressed toast is lost, not shown late.

   :not(.takeover--reopened) excludes the floating reminder's reopened
   confirmation card (_reminder.html.erb, _instruction_card.html.erb) — it
   sits under `hidden` on every accepted attendee's page whether or not it is
   ever tapped open, and :has() matches structural presence, not visibility.
   Without the exclusion this rule would suppress sale toasts for every
   accepted attendee permanently, including through a Chud Checkpoint. CSS has
   no "not a descendant of a hidden ancestor" combinator, so the exclusion is
   a class on the element itself rather than a check on its hidden wrapper —
   see _instruction_card.html.erb's `dismissible` comment.

   That class is not merely static markup, though: it is server-rendered ON
   (matching the card's server-rendered `hidden` state) and then
   participation_reminder_controller.js#expand/#collapse remove and restore
   it to track whether the overlay is actually visible. A card that has been
   tapped open is a genuine full-screen takeover, not a hidden exempt copy
   any more — the exemption has to end when the hiding does, and the
   controller that changes the card's visibility is the only thing that can
   keep the class in step with it (sale-toast-layer). */
body:has(.takeover--quiz) .sale-toasts,
body:has(.takeover--blocking:not(.takeover--reopened)) .sale-toasts,
body:has(#participation_frame [role="dialog"][aria-modal="true"]:not(.takeover)) .sale-toasts {
  display: none;
}

/* A fourth suppression, not a :has() like the three above: this branch moved
   the toast from 1050 to 1070 so it would clear the checkpoint (1065), and
   the vendored .modal (bootstrap.css:5224) sits at 1060 — between the two —
   so the toast now paints over an open modal and can steal a tap meant for
   its button. It cannot simply drop back under 1060, because staying above
   the checkpoint is the point of this change.

   Bootstrap's modal.js adds/removes `modal-open` on <body> itself around
   Modal#show/#hide (bootstrap.bundle.js, CLASS_NAME_OPEN) — the same hook
   every Bootstrap modal on this app already relies on for its own body
   scroll-lock, so this needs no :has() at all. Two real ones exist on the
   attendee surface: the logged-in welcome modal (application.html.erb) and
   the shop's buy-button confirmation. */
body.modal-open .sale-toasts {
  display: none;
}

/* --gp-zIndex-reminder (1040, tokens.css) was already the value inline
   markup used here; wired to the token now that the class exists to hang it
   on. Below every takeover (1055+) deliberately: the reminder is dismissible
   and never the show's own moment. */
.participation-reminder {
  z-index: var(--gp-zIndex-reminder);
}

.sale-toasts {
  z-index: var(--gp-zIndex-sale-toast);
  /* The container spans corner-to-edge of the phone. Without this, an
     attendee tapping a checkpoint's performer button or a quiz answer
     underneath it would hit dead space instead — even while the toast above
     is invisible or suppressed, because the box itself still occupies that
     screen area. */
  pointer-events: none;
}

.sale-toasts > * {
  pointer-events: auto;
}

/* At the bottom-right corner on a phone, a toast lands directly on the
   controls an attendee is actively pressing: the checkpoint's performer
   buttons and confirm, a quiz answer. Moved to the top; left rather than
   right, because the cart icon and navbar toggler already occupy top-right
   on a phone (see application.html.erb's navbar). Desktop keeps the
   bottom-right corner — nothing sits under it there to cover.

   top: 0 used to mean top of the VIEWPORT, which is also where
   application.html.erb's navbar-brand wordmark sits — a persisted
   notification never times out, so one could sit on that link until
   dismissed. --gp-dimension-navbar-clearance (tokens.css) is the navbar's own
   measured bottom edge at this width (see tokens.json for the live
   measurement), so the toast now clears it instead of covering it. The
   navbar is position: relative here, not fixed, so this only matters while
   the page is scrolled to the top — scrolled down, the navbar and its
   wordmark leave the viewport with the rest of the page and the toast (still
   position: fixed) has nothing left to cover.

   env(safe-area-inset-top) is added on top of the token, not folded into it
   (PR #209, main): the token is a measurement of the navbar's own height,
   taken in headless Chrome where every env() reads 0, and .sale-toasts is
   position: fixed — positioned against the VIEWPORT, not the body, so it
   never receives the top padding PR #209 put on <body>. On a notched phone
   the navbar itself is pushed down by that same inset (it is in normal
   flow, under the padded body), so a toast still pinned to the bare token
   would sit back on top of the navbar-brand wordmark, exactly the bug this
   token exists to prevent. Both terms are load-bearing: dropping the env()
   term reproduces the notch regression; dropping the token reproduces the
   original wordmark overlap. See "has .sale-toasts actually consume the
   safe-area inset" in takeover_layering_spec.rb, which fails if either
   disappears.

   overflow: hidden (sale-toast-layer branch) makes this box the clip edge a
   newly inserted toast slides out from under: notifications_controller.js
   starts the toast translated up by its own height (behind this box's top
   edge, i.e. under the navbar) and animates it down to translateY(0), which
   is also its normal, untransformed layout position — the clip does the
   hiding, so nothing here depends on the navbar's own stacking. Harmless
   when no slide is happening (desktop, scrolled, reduced motion): a toast
   with no transform paints entirely inside this box, same as before. Not a
   z-index change and not a new wrapper — this box already sits at the
   navbar's bottom edge, so it doubles as its own clip wrapper. See
   docs/z-index-layers.md ("Where the toasts sit, and why"). */
@media (max-width: 575.98px) {
  .sale-toasts {
    top: calc(env(safe-area-inset-top) + var(--gp-dimension-navbar-clearance)) !important;
    bottom: auto !important;
    left: 0 !important;
    right: auto !important;
    overflow: hidden;
    /* #217 (main, commit 69a07f6) narrowed the id rule below to
       #notifications.bottom-0.end-0.p-3 specifically so it lands only on
       the box actually anchored bottom-right, and stated the reason: padding
       an edge the box is NOT anchored to wastes the full inset "for nothing,
       in landscape on a notched phone." This box still carries the
       bottom-0/end-0 classes in markup (_notifications.html.erb), so it
       still matches that id rule at every width -- but at this width it is
       no longer anchored there. Anchored left now, not right; still anchored
       top, not bottom. So: drop the padding-right/padding-bottom insets back
       to the plain p-3 1rem (the sides are no longer touching those edges),
       and add the left inset for the edge that now applies. No top inset
       here -- the `top:` declaration two lines up already carries
       safe-area-inset-top via --gp-dimension-navbar-clearance; adding it
       again here would double-count it.

       The padding-right/-bottom/-left declarations that used to sit right
       here (commit 98042ae) were DEAD from the moment they were written: at
       .sale-toasts, specificity (0,1,0), they tied on !important with
       #notifications.p-3's (1,1,0) below and lost outright on specificity --
       #notifications.p-3 always won, so the box kept taking right/bottom
       insets on edges it no longer touches. Measured post-merge at 390px
       with stub insets (top 59, right 33, bottom 34, left 35):
       padding-right computed 49px (16+33, #notifications.p-3's value) and
       padding-bottom 50px (16+34), not the 16px this block intended.
       padding-left computed correctly (51px = 16+35) only because nothing
       else competed for that property. Moved below, past
       #notifications.p-3, as `#notifications.sale-toasts` -- see that
       selector for why. */
  }
}

/* The chosen character, ringed in their own colour and lifted.
   --performer-accent is set per button and already falls back when
   icon_color is blank or white; see PerformersHelper#checkpoint_accent. */
.takeover--checkpoint [data-checkpoint-target="performerButton"] img {
  border: 3px solid transparent;
  transition: transform 120ms ease, border-color 120ms ease;
}

.takeover--checkpoint [data-checkpoint-target="performerButton"].is-selected img {
  border-color: var(--performer-accent, var(--gp-accent-fallback));
  transform: scale(1.08);
}

/* ---------------------------------------------------------------------------
   Safe areas.

   layouts/application sets viewport-fit=cover, so every page on that layout
   paints under the iPhone's status bar, notch and home indicator. These keep
   what is pinned to an edge out from under them. Every env() is 0 on a desktop,
   on an older phone and on the four layouts that do NOT set the flag
   (store, minimum, admin, admin_login), so nothing moves there.

   The top inset is padding on the BODY rather than a margin on each thing that
   happens to be first -- the navbar when signed in, the Gross Prophets logo
   block when signed out, the page itself on a layout that renders neither.
   checkpoint-ui-polish pushes the navbar down instead, which is the same result
   for the one case it had; it has no logged-out page to keep out of the notch.
   MERGING THE TWO: take one or the other, not both, or a signed-in phone gets
   the inset twice.

   The body's background is untouched by its own padding -- a background on the
   body propagates to the canvas and paints the whole viewport including the
   insets -- which is what lets .gp-sky run under the status bar while the form
   inside it does not. See .gp-sky in components.css for the colour Safari reads.
   --------------------------------------------------------------------------- */
body {
  padding-top: env(safe-area-inset-top);
  padding-left: env(safe-area-inset-left);
  padding-right: env(safe-area-inset-right);
}

/* The footer, not the body, takes the bottom inset: it is `bg-light` and runs
   edge to edge, so padding keeps its fill under the home indicator where body
   padding would leave a strip of page background below it. `py-3` is a
   Bootstrap utility and therefore !important; this has to be too. */
footer.footer {
  padding-bottom: calc(1rem + env(safe-area-inset-bottom)) !important;
}


/* The notification box is position: fixed at bottom-0 end-0, so it is the one
   thing the body's padding cannot move. p-3 is a utility and !important; the id
   outranks it on specificity, but importance is decided first, hence both.

   Deliberately (1,1,0), NOT narrowed to the anchor classes. Two elements carry
   id="notifications", and the other one -- welcome/order_notifications.html.erb:4,
   pinned top-0 start-0 -- is handled by its own rule below rather than by making
   this selector more specific. Narrowing this one to
   `#notifications.bottom-0.end-0.p-3` was tried and reverted: at (1,3,0) it
   outranks the operator row's `#shop .p-3` at (1,1,0), specificity is resolved
   before source order, and the row therefore stopped masking it -- so on /shop
   the box went to 8px top/left and 16px right/bottom, asymmetric, live, before
   any cutover. Measured in the real link order, not reasoned about. At (1,1,0)
   the row wins on /shop again until its block is emptied, which is when this
   change is meant to happen and is what design/parity/shop/accepted.json
   records. See docs/attendee-design-system-plan.md section 3.5 for why a
   body-scoped row rule reaches shared chrome at all. */
#notifications.p-3 {
  padding-right: calc(1rem + env(safe-area-inset-right)) !important;
  padding-bottom: calc(1rem + env(safe-area-inset-bottom)) !important;
}

/* At the mobile width where the box re-anchors top-left (the @media block
   above .sale-toasts), the right/bottom insets #notifications.p-3 sets
   above no longer belong on this box -- see that block's comment for the
   defect this replaces and the numbers that proved it dead.

   `#notifications.sale-toasts` is (1,1,0): the SAME specificity as
   #notifications.p-3 above, deliberately, not higher. Both carry
   !important, so importance ties and specificity ties too, which means
   source order decides -- and this rule is declared after
   #notifications.p-3, so it wins the padding-right/-bottom tie on this box
   at this width. It is also (1,1,0) against the operator row's
   `#shop .p-3` -- see #notifications.p-3's comment above for why that tie
   is deliberate and must not be broken by going higher. That tie is not
   decided here, though: the row is Setting[code: "custom_css"], rendered in
   its own <style> tag after every linked stylesheet including this one
   (application.html.erb) -- so on /shop it is always the last-loaded
   #shop .p-3 declaration in the document, regardless of where within THIS
   file our rule sits, and it keeps winning there exactly as it does on
   main today. */
@media (max-width: 575.98px) {
  #notifications.sale-toasts {
    padding-right: 1rem !important;
    padding-bottom: 1rem !important;
    padding-left: calc(1rem + env(safe-area-inset-left)) !important;
  }
}

/* The OTHER #notifications box: welcome/order_notifications.html.erb:4 renders one
   pinned top-0 start-0. The rule above matches it too, and padded the two sides it
   is not anchored to -- up to the full inset for nothing, in landscape on a notched
   phone. This takes those two back to the p-3 value rather than making the rule
   above more specific, because raising that one's specificity is what took it past
   the operator row. (1,3,0) here is fine: nothing else sets padding on this box. */
#notifications.top-0.start-0.p-3 {
  padding-right: 1rem !important;
  padding-bottom: 1rem !important;
}
