/* v3.60.0 — item 5, 6th-attempt fix for "design / prepare / live
 * look different on larger screens". The rule below reserves space
 * for the page-level scrollbar even when no scrollbar is needed,
 * so toggling between short content (design mode with few sessions)
 * and tall content (prepare mode with many participants) doesn't
 * shift the layout by ~15-17px when the browser shows/hides the
 * scrollbar. Cross-browser: Chrome 94+, Edge 94+, Firefox 97+,
 * Safari 18+. On older Safari the issue persists but is mild and
 * mac users typically don't see it (overlay scrollbars). Not paired
 * with `body { overflow-y: scroll }` because the body uses flex
 * column for the sidebar layout and adding overflow there moves the
 * scroll container in a way that breaks sticky elements. */
html { scrollbar-gutter: stable; }

/* =============================================================================
 * SF Platform — brand overrides on Bootstrap 5
 * Brand: #0DA5F2 (Solving Forces cyan)
 * ===========================================================================*/

/* v16.30.0 — the Solving Forces palette for every calc/viz picture (src/viz/theme.ts
   carries the same values as inline fallbacks, so a printed or unstyled page matches).
   TONE = text-safe inks (≥ 4.5:1 on white and on the cream); AREA = brand colours at
   full saturation for bars, bands, dots, cells; SERIES = the categorical order. */
:root {
  --calc-fill: #177891;        /* teal ink (from 30·156·189) */
  --calc-fill-soft: #a7d8e6;
  --calc-good: #2e7e35;        /* green ink (from 64·175·73) */
  --calc-warn: #8f6904;        /* amber ink (from 249·186·20) */
  --calc-bad: #c64511;         /* orange ink (from 236·93·35) */
  --calc-highlight: #75489c;   /* purple 117·72·156 — text-safe as-is */
  --calc-ink: #1f2937;
  --calc-muted: #6b7280;
  --calc-grid: #e5e7eb;
  --calc-paper: #fcf4e4;       /* cream 252·244·228 */
  --calc-area-fill: #1e9cbd;
  --calc-area-good: #40af49;
  --calc-area-warn: #f9ba14;
  --calc-area-bad: #ec5d23;
  --calc-area-highlight: #75489c;
  --calc-area-soft: #a7d8e6;
  --calc-series-1: #1e9cbd; --calc-series-2: #75489c; --calc-series-3: #ec5d23; --calc-series-4: #f9ba14; --calc-series-5: #40af49;
}

:root {
  --sf-brand: #0DA5F2;
  --sf-brand-dark: #0884C2;
  --bs-primary: #0DA5F2;
  --bs-primary-rgb: 13, 165, 242;
}

/* ===== Layout ===== */
body {
  min-height: 100vh;
  display: flex;
  flex-direction: column;
}

/* Mobile: offset for fixed top bar */
.sf-main {
  padding-top: 56px;
  flex: 1;
}

/* Desktop sidebar — fixed column */
.sf-sidebar {
  position: fixed;
  top: 0;
  left: 0;
  bottom: 0;
  width: 220px;
  z-index: 1030;
  overflow-y: auto;
}

/* Desktop: main content shifts right, no top padding */
@media (min-width: 992px) {
  .sf-main {
    margin-left: 220px;
    padding-top: 0;
  }
}

/* ===== Brand buttons =====
 *
 * v4.8.0 — Backlog bug #9 root cause. The block below originally
 * customised only --bs-btn-bg, --bs-btn-border-color, --bs-btn-hover-bg,
 * --bs-btn-hover-border-color. Bootstrap 5's .btn-primary defines SIX+
 * background/border slots and unset ones fall back to Bootstrap's
 * built-in deep-navy #0a58ca instead of the brand cyan, so:
 *   - Mode-switcher Live tab (.btn-primary.active) → wrong blue.
 *   - Any future <button disabled class="btn-primary"> → wrong blue.
 *   - Any focused-and-pressed state → wrong blue.
 * The complete set of slots a .btn-X reset uses (Bootstrap 5.3 source,
 * scss/_buttons.scss button-variant mixin — keep this list current if
 * you bump Bootstrap):
 *
 *   --bs-btn-bg                       --bs-btn-color
 *   --bs-btn-hover-bg                 --bs-btn-hover-color
 *   --bs-btn-active-bg                --bs-btn-active-color
 *   --bs-btn-disabled-bg              --bs-btn-disabled-color
 *   --bs-btn-border-color             --bs-btn-hover-border-color
 *   --bs-btn-active-border-color      --bs-btn-disabled-border-color
 *
 * Setting bg/hover-bg alone is the trap that produced #9; set the
 * active and disabled slots too. The brand only has two shades
 * (#0DA5F2 base, #0884C2 dark) and white text, which maps cleanly:
 *
 *   base/hover-border/active/disabled bg → brand or brand-dark
 *   text (color)                          → white (default)
 *   disabled                              → brand at 0.65 alpha is
 *                                            close enough; we use
 *                                            brand-dark to keep
 *                                            contrast and a clear
 *                                            "this is the brand" cue.
 */
.btn-primary {
  --bs-btn-bg: var(--sf-brand);
  --bs-btn-border-color: var(--sf-brand);
  --bs-btn-hover-bg: var(--sf-brand-dark);
  --bs-btn-hover-border-color: var(--sf-brand-dark);
  /* v4.8.0 — active-* and disabled-* added (see comment above) */
  --bs-btn-active-bg: var(--sf-brand-dark);
  --bs-btn-active-border-color: var(--sf-brand-dark);
  --bs-btn-disabled-bg: var(--sf-brand);
  --bs-btn-disabled-border-color: var(--sf-brand);
}

.btn-outline-primary {
  --bs-btn-color: var(--sf-brand);
  --bs-btn-border-color: var(--sf-brand);
  --bs-btn-hover-bg: var(--sf-brand);
  --bs-btn-hover-border-color: var(--sf-brand);
  /* v4.8.0 — active-* added so the "selected" outline-primary tabs
     don't fall back to Bootstrap navy. The text on active is white
     (Bootstrap's button-outline-variant default) so we don't need
     --bs-btn-active-color. */
  --bs-btn-active-bg: var(--sf-brand-dark);
  --bs-btn-active-border-color: var(--sf-brand-dark);
  --bs-btn-disabled-color: var(--sf-brand);
}

/* ===== Challenge rows — bold state indication =====
   v3.43.2 — left-stripe removed so cards have full border chrome
   matching the design-mode visual. State is signalled by background tint
   and the inline Bootstrap border-*-subtle classes the templates emit. */
.challenge-row {
  transition: background-color 0.2s;
}
.challenge-row[data-status="active"] {
  background-color: rgba(var(--bs-primary-rgb), 0.06) !important;
}
.challenge-row[data-status="closed"] {
  background-color: rgba(var(--bs-warning-rgb), 0.06) !important;
}
/* v8.3.2 — a not-yet-live challenge is greyed via its HEADLINE (name, number,
   thumbnail) rather than the whole <li>. opacity on the row would composite the
   action buttons (groups / timer / Start) too, dimming them; scoping the
   opacity to .challenge-headline keeps those controls at full strength so they
   stay obviously clickable. (Closed challenges still use opacity-50 on the row
   — they have no actionable controls.) */
.challenge-row[data-status="inactive"] .challenge-headline {
  opacity: 0.6;
}

/* ===== Element visibility toggles ===== */
.element-toggle-list {
  display: flex;
  flex-direction: column;
  gap: 2px;
}
.element-toggle {
  display: flex;
  align-items: flex-start;
  gap: 6px;
  padding: 4px 0;
  font-size: 0.85rem;
  cursor: pointer;
}
.element-type-tag {
  display: inline-block;
  font-size: 0.7rem;
  padding: 1px 4px;
  border-radius: 3px;
  font-family: monospace;
  vertical-align: middle;
}

/* ===== Invitation list ===== */
.invitation-item .invitation-url {
  word-break: break-all;
  font-size: 0.8rem;
}

/* ===== Participant pages ===== */
.participant-logo {
  height: 28px;
  width: auto;
}

/* Content vs interaction distinction */
.content-card {
  border-left: 3px solid var(--bs-secondary-bg);
}
.interact-card {
  border-left: 3px solid var(--sf-brand);
  background-color: rgba(var(--bs-primary-rgb), 0.03);
}

/* Magic word page — dark entrance feel, works within body flex */
.magic-word-page {
  background: linear-gradient(135deg, #1a1a2e 0%, #16213e 50%, #0f3460 100%);
  flex: 1;
  min-height: 100dvh;
}
.magic-word-page ~ footer {
  background: #0f3460;
  border-top-color: rgba(255,255,255,0.1) !important;
}
.magic-word-page ~ footer small,
.magic-word-page ~ footer a {
  color: rgba(255,255,255,0.35) !important;
}

/* ===== Pause modal ===== */
.pause-modal {
  position: fixed;
  inset: 0;
  background: rgba(0, 0, 0, 0.5);
  display: flex;
  align-items: center;
  justify-content: center;
  z-index: 1055;
}

/* ===== Highlight animation for new content ===== */
.highlight {
  animation: sf-highlight 2s ease-out;
}
@keyframes sf-highlight {
  0% { background-color: #fffbcc; }
  100% { background-color: transparent; }
}

/* ===== Touch targets — minimum 44px ===== */
@media (pointer: coarse) {
  .btn-sm {
    min-height: 44px;
    min-width: 44px;
    display: inline-flex;
    align-items: center;
    justify-content: center;
  }
  .form-check-input {
    width: 1.25em;
    height: 1.25em;
  }
}

/* ===== Logo — invert to white only in dark navbars ===== */
.bg-dark .navbar-brand img {
  filter: brightness(0) invert(1);
}

/* ===== Participant challenge — element styles ===== */
.markdown-content > :first-child { margin-top: 0; }
.markdown-content h4 { font-size: 1.1rem; }
.markdown-content h5 { font-size: 1rem; }
.markdown-content code { background: #f0f0f0; padding: 0.1em 0.3em; border-radius: 3px; font-size: 0.9em; }

.sf-img-loading { min-height: 80px; background: #f8f9fa; display: flex; align-items: center; justify-content: center; }
.sf-img-loading::after { content: ''; width: 24px; height: 24px; border: 2px solid #dee2e6; border-top-color: #0da5f2; border-radius: 50%; animation: sf-spin 0.7s linear infinite; }
@keyframes sf-spin { to { transform: rotate(360deg); } }

/* v5.0.17 U1-full — stacked upload modal over image picker.
   Bootstrap's default .modal z-index is 1055 + backdrop 1050. When
   a second modal opens on top, it needs to clear the first one.
   The .image-picker-upload-modal class bumps both above the
   picker's defaults so the stacking works visually. Bootstrap's
   own modal manager handles focus trapping + scroll lock correctly
   for nested modals as of v5; only z-index needs adjustment. */
.modal.image-picker-upload-modal {
  z-index: 1075;
}
.modal-backdrop.image-picker-upload-backdrop {
  z-index: 1070;
}

/* v5.0.11 U7 — skeleton shimmer for library thumbnails. Subtle vs
   sf-img-loading's spinner because the library renders many cards
   at once; spinners-per-card would be visually noisy. The img's
   load event removes the class via the data-img-fallback listener
   in images-library.js, revealing the image with no fade required
   (the img itself takes over the same box). */
.sf-thumb-loading {
  position: relative;
  overflow: hidden;
}
.sf-thumb-loading::before {
  content: '';
  position: absolute;
  inset: 0;
  background: linear-gradient(
    90deg,
    rgba(248, 249, 250, 1) 0%,
    rgba(233, 236, 239, 1) 50%,
    rgba(248, 249, 250, 1) 100%
  );
  background-size: 200% 100%;
  animation: sf-thumb-shimmer 1.2s linear infinite;
  /* Above the .bg-light but below the img once it loads */
  z-index: 1;
}
.sf-thumb-loading > img {
  position: relative;
  z-index: 2; /* keeps img above the shimmer; once loaded the
                 parent loses .sf-thumb-loading and ::before goes
                 away entirely. */
}
@keyframes sf-thumb-shimmer {
  0%   { background-position: 100% 0; }
  100% { background-position: -100% 0; }
}

.sf-role-badge { cursor: pointer; transition: background 0.15s, color 0.15s; padding: 0.3em 0.6em; font-size: 0.8rem; }
.sf-role-badge:hover { background: #343a40; color: #fff; }

/* ===== Group Setup Modal ===== */

/* Grid: 2 columns on tablet+, 1 on mobile */
.gs-grid {
  display: grid;
  grid-template-columns: 1fr;
  gap: 10px;
}
@media (min-width: 640px) {
  .gs-grid { grid-template-columns: 1fr 1fr; }
}

/* Group card */
.gs-group-card {
  border: 1px solid var(--bs-border-color, #dee2e6);
  border-radius: 8px;
  overflow: hidden;
  background: var(--bs-body-bg, #fff);
}

/* Headers */
.gs-group-header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: 6px 10px;
  font-size: 0.85rem;
  font-weight: 600;
}
.gs-header-unlocked { background: rgba(var(--bs-primary-rgb, 13,165,242), 0.1); border-bottom: 2px solid var(--sf-brand, #0DA5F2); }
.gs-header-locked   { background: rgba(var(--bs-success-rgb, 25,135,84), 0.1);  border-bottom: 2px solid var(--bs-success, #198754); }
.gs-header-onhold   { background: var(--bs-body-tertiary, #f8f9fa); border-bottom: 2px solid var(--bs-secondary-bg, #e9ecef); }

.gs-group-title { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.gs-group-code { font-weight: 400; font-size: 0.7rem; color: var(--bs-secondary-color, #6c757d); margin-left: 4px; }

.gs-group-actions { display: flex; align-items: center; gap: 2px; flex-shrink: 0; }

.gs-icon-btn {
  background: none;
  border: none;
  padding: 2px 5px;
  cursor: pointer;
  font-size: 0.8rem;
  color: var(--bs-secondary-color, #6c757d);
  border-radius: 4px;
  transition: background 0.15s, color 0.15s;
}
.gs-icon-btn:hover { background: var(--bs-body-tertiary, #f8f9fa); color: var(--bs-body-color, #212529); }
.gs-icon-danger:hover { background: rgba(220,53,69,0.1); color: #dc3545; }

@media (pointer: coarse) {
  .gs-icon-btn { min-width: 38px; min-height: 38px; font-size: 1rem; display: inline-flex; align-items: center; justify-content: center; }
}

/* Member list */
.gs-member-list {
  min-height: 42px;
  padding: 4px;
}
.gs-locked-list { opacity: 0.75; }

.gs-member-item {
  display: flex;
  align-items: center;
  gap: 6px;
  padding: 4px 6px;
  border-radius: 5px;
  font-size: 0.82rem;
  background: var(--bs-body-bg, #fff);
  margin-bottom: 2px;
  transition: background 0.1s, border-color 0.1s, box-shadow 0.1s;
}
.gs-member-item:last-child { margin-bottom: 0; }

/* v4.4.4 — item 5: affected participants get a subtle yellow tint
   when their assignment differs from the last-saved state. The
   leader sees at a glance which participants will be moved when
   they save. The highlight clears automatically when the change
   is undone (drag back, or Reset). Same yellow-warning family
   as the reveal-panel's "changed row" pattern, for visual
   consistency across "you have pending changes" UI.
   The 3px left-border (instead of full border) keeps the card
   the same width as unaffected siblings — no layout jitter when
   the highlight appears/disappears. */
.gs-member-item.gs-affected {
  background: rgba(var(--bs-warning-rgb, 255, 193, 7), 0.10);
  box-shadow: inset 3px 0 0 0 rgba(var(--bs-warning-rgb, 255, 193, 7), 0.55);
}

.gs-drag-handle {
  cursor: grab;
  color: var(--bs-secondary-color, #adb5bd);
  font-size: 1rem;
  padding: 2px;
  display: flex;
  align-items: center;
}
.gs-drag-handle:active { cursor: grabbing; }

@media (pointer: coarse) {
  .gs-drag-handle { padding: 6px; font-size: 1.2rem; min-width: 44px; min-height: 44px; display: flex; align-items: center; justify-content: center; }
}

.gs-member-name { font-weight: 500; }

.gs-pos-num { font-size: 0.7rem; color: var(--bs-secondary-color, #adb5bd); min-width: 1.5em; text-align: right; flex-shrink: 0; }

.gs-role-badge {
  font-size: 0.7rem;
  padding: 1px 6px;
  border-radius: 3px;
  background: rgba(var(--bs-success-rgb, 25,135,84), 0.12);
  color: var(--bs-success-text-emphasis, #0a3622);
  white-space: nowrap;
}

.gs-empty-hint {
  text-align: center;
  padding: 12px 0;
  font-size: 0.75rem;
  color: var(--bs-secondary-color, #adb5bd);
  font-style: italic;
}

/* Sortable.js states */
.gs-ghost { opacity: 0.3; }
.gs-chosen { box-shadow: 0 2px 8px rgba(0,0,0,0.15); }

/* Editor element-card drag states. Same idea as gs-* but tuned for the
   exercise editor: a slightly more visible ghost (the cards are larger and
   the placeholder needs to be obvious) and a clearer chosen halo. */
.element-card-ghost {
  opacity: 0.4;
  background: var(--bs-tertiary-bg) !important;
  border-style: dashed !important;
}
.element-card-chosen {
  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.18);
  cursor: grabbing !important;
}
.gs-drag { opacity: 0.9; }

/* Generate bar */
.gs-generate-bar {
  background: var(--bs-body-tertiary, #f8f9fa);
  border-radius: 6px;
  padding: 8px 12px;
  margin-bottom: 10px;
}

/* Role checkboxes */
.gs-role-check {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  font-size: 0.8rem;
  cursor: pointer;
  padding: 2px 8px;
  border-radius: 4px;
  background: var(--bs-body-tertiary, #f8f9fa);
}
.gs-role-check input { margin: 0; }

.gs-radio-label { font-size: 0.82rem; display: inline-flex; align-items: center; gap: 3px; }
.gs-radio-label input { margin: 0; }

/* On-hold card — subtle distinction */
.gs-onhold-card { border-style: dashed; }

/* Challenge groups section in cockpit */
.gs-challenge-groups { border-top: 1px solid var(--bs-border-color, #dee2e6); padding-top: 6px; }

/* ===== Cockpit mode system ===== */

/* Live now card — prominent red border, subtle pulse */
.live-now-card { border-width: 2px; }
.live-now-card .card-header { font-size: 0.9rem; }

@keyframes sf-live-pulse {
  0%, 100% { opacity: 1; }
  50% { opacity: 0.7; }
}
.live-now-card .bi-broadcast { animation: sf-live-pulse 2s ease-in-out infinite; }

/* Mode switcher active states */
/* v3.44.28 — restored visual clarity. Pre-3.44.28 inactive buttons were
 * dimmed with `opacity: 0.55` which left their white-on-coloured text
 * effectively unreadable (especially btn-primary's white-on-blue Live
 * button — the user reported "Live button text is not readable"). The
 * active state used only `font-weight: 600` which was too subtle to
 * spot. Replaced with a clean toggle pattern:
 *
 *   INACTIVE: outline-style — transparent background, coloured border
 *             and coloured text. Fully readable, low visual weight.
 *   ACTIVE:   filled background as Bootstrap defaults (warning/success/
 *             primary), bolder font, and a subtle inset bottom shadow
 *             to mark it as the "current view" the way browser tabs do.
 *
 * The btn-warning / btn-success / btn-primary classes stay on the
 * markup; this CSS overrides the inactive state to flip the fill.
 */
/* v12.0.200 (screenshot round 2, item 3) — constant font-weight across all three
 * mode tabs so switching (which used to bump the active tab 400→700) no longer
 * changes any button's width. Active is signalled by the filled colour + the
 * inset 3px underline below, not weight. Replaces the v12.0.191 hidden-pseudo
 * width-reserve, which didn't fully prevent the reflow across browsers. */
#mode-switcher .btn { font-weight: 600; }
#mode-switcher .btn.active {
  box-shadow: inset 0 -3px 0 rgba(0, 0, 0, 0.18);
}
/* Subtle scale-up so the active button feels "pressed forward" */
#mode-switcher .btn.active:not(:hover) {
  z-index: 1;
}

/* v3.44.15 (item 3) — consistent cockpit-mode padding across Design /
 * Prepare / Live. Previously Design felt visually more "padded" on the
 * right than Prepare/Live, likely because Design's Edit-button row sits
 * right-aligned at the top (creating an anchored right-edge element)
 * while Prepare/Live's first content (nav-tabs, live-now-card) extended
 * full-width with edge-aligned bottom borders. The container-fluid
 * already gives symmetric horizontal padding; this rule just guarantees
 * none of the inner content can override it with negative margins or
 * full-bleed wrappers. Targets the .cockpit-mode wrapper directly. */
.cockpit-mode {
  padding-inline: 0;        /* explicit zero — the container above pads */
  box-sizing: border-box;
}
.cockpit-mode > * {
  max-width: 100%;          /* no inner element exceeds the wrapper's box */
}

/* v3.55.1 — removed the v3.55.0 per-block padding-inline rule. User
 * feedback: "all 3 are different. The padding/margin should be the
 * standard. Rebuild prepare and live on the design layout." v3.55.0
 * gave only prepare/live extra padding-inline, leaving design without
 * — visible inconsistency between modes. Reverted to the design-mode
 * baseline (no per-block padding-inline); all three modes now derive
 * horizontal space solely from the parent .container-fluid's
 * responsive padding (px-3 px-sm-4 px-md-5). Identical across modes. */

/* v3.44.14 — mode-tab dimming. Inactive tabs in the Design/Prepare/Live
 * switcher are filled-colour at 0.55 opacity; the active tab is full
 * opacity. Replaces the v3.44.10 outline-vs-filled distinction. The 3px
 * indicator bar below the header remains the strongest active-mode
 * signal. */
/* v3.44.28 — outline-style inactive state. Each btn-X variant gets its
 * accent colour as text and border, transparent fill. Fully readable
 * (no opacity dimming the white text on coloured fill). The active
 * state above keeps the Bootstrap defaults (filled coloured bg, white
 * text) so the contrast between active and inactive is clear.
 *
 * The hover state for inactive buttons adds a tiny fill hint without
 * dimming the text — better than the old 0.55 → 0.85 opacity dance.
 */
#mode-switcher .btn.mode-tab-inactive.btn-warning {
  background-color: transparent;
  color: var(--bs-warning-text-emphasis, #997404);
  border-color: #fff; /* v15.11.0 (owner request 8) — white outlines on the tinted page */
}
#mode-switcher .btn.mode-tab-inactive.btn-success {
  background-color: transparent;
  color: var(--bs-success-text-emphasis, #146c43);
  border-color: #fff; /* v15.11.0 (owner request 8) — white outlines on the tinted page */
}
#mode-switcher .btn.mode-tab-inactive.btn-primary {
  background-color: transparent;
  color: var(--bs-primary-text-emphasis, #084298);
  border-color: #fff; /* v15.11.0 (owner request 8) — white outlines on the tinted page */
}
/* v12.0.200 — inactive tabs fill with their own colour on hover (standard
   Bootstrap outline-button behaviour), instead of a faint grey wash. The
   dormant Live tab (:not(.mode-tab-is-live), higher specificity below) keeps
   its grey hover so it stays recessive when nothing is running. */
#mode-switcher .btn.mode-tab-inactive.btn-warning:hover {
  background-color: var(--bs-warning); color: #000; border-color: var(--bs-warning);
}
#mode-switcher .btn.mode-tab-inactive.btn-success:hover {
  background-color: var(--bs-success); color: #fff; border-color: var(--bs-success);
}
#mode-switcher .btn.mode-tab-inactive.btn-primary:hover {
  background-color: var(--bs-primary); color: #fff; border-color: var(--bs-primary);
}

/* item 3b — the Prepare-mode sub-tabs (Outline / Invite / Waiting room /
   Participants) get a green active tab (white text) and a green baseline,
   instead of the default grey, matching "prepare = green" across the cockpit. */
#prepare-tabs.nav-tabs { border-bottom: 2px solid var(--bs-success); }
#prepare-tabs .nav-link.active {
  background-color: var(--bs-success);
  border-color: var(--bs-success);
  color: #fff;
}
#prepare-tabs .nav-link.active .badge {
  background-color: #fff !important;
  color: var(--bs-success) !important;
}

/* item 3e — the live "Live elements" panel reads brand-blue at the top and
   bottom only, with a plain (unfilled) body — like a participant interactive
   element but blue instead of indigo, rather than a fully blue-filled box. The
   header text/icon stay brand-blue (set on the toggle button). */
.sf-live-panel.card {
  background: transparent;
  border: none;
  border-radius: 0;
}

/* v8.3.2 — #1d: the Live tab has three states.
   • active (on the Live page) → filled blue + white (the .active default).
   • inactive + something live (.mode-tab-is-live) → blue outline + bold +
     subtle pulse (handled by .mode-tab-inactive.btn-primary + .mode-tab-is-live).
   • inactive + nothing live → grey, so the Live tab recedes when nothing is
     running. This overrides the blue-outline inactive style for that case only.
   hasLive is server-rendered and refreshed on go-live / stop (the cockpit
   reloads on those), so the state stays accurate. */
#mode-switcher .btn.mode-tab-inactive.btn-primary:not(.mode-tab-is-live) {
  color: var(--bs-secondary-color, #6c757d);
  border-color: #fff; /* v15.11.0 (owner request 8) */
}
/* v15.11.0 (owner request 8) — the ONE coloured outline: Live, while
   something is actually live. Everything else is white. */
#mode-switcher .btn.mode-tab-inactive.btn-primary.mode-tab-is-live {
  border-color: var(--bs-primary);
}
#mode-switcher .btn.mode-tab-inactive.btn-primary:not(.mode-tab-is-live):hover {
  background-color: rgba(var(--bs-tertiary-bg-rgb, 233, 236, 239), 0.5);
  /* v12.0.206 (audit F4) — explicit, so the dormant tab's grey hover no longer
     depends on this rule sitting AFTER the v200 blue-fill hover in the file
     (equal specificity for color/border; order was the only tiebreak). */
  color: var(--bs-secondary-color, #6c757d);
  border-color: var(--bs-border-color, #dee2e6);
}

/* Modal body scroll: on desktop, cap height; on mobile fullscreen, use flex */
@media (min-width: 768px) {
  #groupSetupModal .modal-body { max-height: 75vh; }
}
#groupSetupModal .modal-content { display: flex; flex-direction: column; }
#groupSetupModal .modal-body { flex: 1; min-height: 0; }

/* ============================================================================
 * v3.9.1 — mobile + accessibility quick pass
 * ============================================================================
 *
 * Mobile touch targets: WCAG 2.5.5 (Target Size, AAA) recommends a minimum
 * 44×44 CSS pixels for interactive controls. Bootstrap's `.btn-sm` sits
 * around 31px tall, which is fine on desktop but tight for thumbs on a
 * phone. The rule below upsizes small buttons inside lists/cards on
 * touch-primary devices — desktop with a mouse keeps the compact layout.
 */

@media (hover: none) and (pointer: coarse) {
  /* Touch-primary device — bump small buttons to a comfortable target.
     We narrow the selector to buttons inside cards/list-groups to avoid
     over-inflating dense table actions where space matters. */
  .card .btn-sm,
  .list-group-item .btn-sm,
  .input-group .btn-sm {
    min-height: 44px;
    min-width: 44px;
  }

  /* Form controls in card bodies likewise — Bootstrap form-control-sm is
     ~31px; on touch we want the larger 38px default or above. */
  .card .form-control-sm,
  .card .form-select-sm {
    min-height: 38px;
  }
}

/*
 * iOS Safari <details> disclosure-marker is a tiny touch target. v3.8.0
 * uses <details open> for participant session blocks; the summary needs
 * to be a comfortable touch target for the participant to collapse/expand.
 *
 * The fix: hide the native marker, add a custom one that's part of the
 * summary's clickable area, and pad the summary itself so the whole
 * block is tappable.
 */
details > summary {
  list-style: none;          /* Firefox: hide default ▶ */
  cursor: pointer;
  padding: 0.5rem 0.25rem;   /* Pad so the whole row is a 44px+ target */
  position: relative;
  padding-right: 1.75rem;    /* Room for our custom marker */
}
details > summary::-webkit-details-marker {
  display: none;             /* Safari/Chromium: hide default ▶ */
}
details > summary::after {
  content: "›";
  position: absolute;
  right: 0.5rem;
  top: 50%;
  transform: translateY(-50%) rotate(90deg);
  transition: transform 0.15s ease;
  font-size: 1.2rem;
  color: var(--bs-secondary);
}
details[open] > summary::after {
  transform: translateY(-50%) rotate(-90deg);
}


/* v12.0.327 (LRN.327) — the collapsible GREEN section header, shared by the
   design + live learning cards: full green when collapsed, light green when
   open, mirroring the participant interactive-element colour idea but in the
   success hue. Uses the same ::after chevron as other details summaries. */
/* v15.11.0 (owner request 7) — SWAPPED: light when COLLAPSED, full colour
   when OPEN. An open block is the one you're working in; it gets the
   emphasis. (Pre-15.11 it was the other way around.) */
/* v15.15.0 (field report 2) — the open panels' outline matches the live
   challenge cards' 2px (.live-now-card); collapsed stays the quiet 1px. */
details.sf-collapse-green[open],
details.sf-collapse-yellow[open] { border-width: 2px; }
details.sf-collapse-green > summary.sf-collapse-green-summary {
  background: rgba(25, 135, 84, 0.10);
  color: var(--bs-success);
  border-color: rgba(25, 135, 84, 0.25);
  cursor: pointer;
}
details.sf-collapse-green > summary.sf-collapse-green-summary .bi { color: var(--bs-success); }
details.sf-collapse-green[open] > summary.sf-collapse-green-summary {
  background: var(--bs-success);
  color: #fff;
  border-bottom: 1px solid var(--bs-success);
}
details.sf-collapse-green[open] > summary.sf-collapse-green-summary .bi { color: #fff; }

/* v14.59.3 (report 2) — the GREY collapsible, same mechanics as the green
   one above. For journey-wide SETTINGS (currently the participant language),
   which are configuration rather than session activity: grey keeps them
   visually quieter than the green Learning card and the yellow leaderboard,
   which is the point — you set them once. */
details.sf-collapse-grey > summary.sf-collapse-grey-summary {
  background: var(--bs-secondary-bg, #e9ecef);
  color: var(--bs-body-color);
  border-color: var(--bs-border-color);
  cursor: pointer;
}
details.sf-collapse-grey > summary.sf-collapse-grey-summary .bi { color: var(--bs-secondary-color, #6c757d); }
details.sf-collapse-grey[open] > summary.sf-collapse-grey-summary {
  background: rgba(108, 117, 125, 0.10);
  border-bottom: 1px solid rgba(108, 117, 125, 0.25);
}
.sf-collapse-green-count {
  background: rgba(25, 135, 84, 0.15);
  color: var(--bs-success);
}
details.sf-collapse-green[open] .sf-collapse-green-count {
  background: rgba(255, 255, 255, 0.25);
  color: #fff;
}
/* a "live" (participant-visible) learning row reads as active, like a live
   challenge row: a faint success tint so the Live badge isn't the only cue. */
.sf-learning-live { background: rgba(25, 135, 84, 0.04); }

/* v12.0.329 (LRN.329) — the collapsible YELLOW header for the XP Leaderboard:
   full warning when collapsed, light warning when open, star icon. Parallels
   .sf-collapse-green. */
/* v15.11.0 (owner request 7) — swapped, same as the green family. */
details.sf-collapse-yellow > summary.sf-collapse-yellow-summary {
  background: rgba(255, 193, 7, 0.14);
  color: #664d03;
  border-color: rgba(255, 193, 7, 0.35);
  cursor: pointer;
}
details.sf-collapse-yellow > summary.sf-collapse-yellow-summary .bi { color: #997404; }
details.sf-collapse-yellow[open] > summary.sf-collapse-yellow-summary {
  background: var(--bs-warning);
  color: #3d2c00;
  border-bottom: 1px solid var(--bs-warning);
}
details.sf-collapse-yellow[open] > summary.sf-collapse-yellow-summary .bi { color: #3d2c00; }
.sf-lb-preview { background: rgba(255, 193, 7, 0.05); }

/*
 * Skip-to-content link: ensure it shows clearly when focused. Bootstrap's
 * .visually-hidden-focusable handles the show/hide; we just give it a
 * visible style so keyboard users see where it lands.
 */
.visually-hidden-focusable:focus,
.visually-hidden-focusable:active {
  outline: 2px solid var(--sf-brand);
  outline-offset: 2px;
}

/* v12.0.129 — SR-only utility (mirrors Bootstrap's .visually-hidden) for status
 * conveyed visually by colour/icon, e.g. the responses "Who's in it" answered/
 * pending chips. Explicit here so it works regardless of the Bootstrap build. */
.visually-hidden {
  position: absolute !important;
  width: 1px !important;
  height: 1px !important;
  padding: 0 !important;
  margin: -1px !important;
  overflow: hidden !important;
  clip: rect(0, 0, 0, 0) !important;
  white-space: nowrap !important;
  border: 0 !important;
}

/* Print stylesheet — minimal, just hide nav/buttons so a leader
 * can print an exercise as a handout (BACKLOG 🟢 — partial coverage). */
@media print {
  /* v3.9.1 — basic print suppression. v3.16.1 — extended for
   * exercise handouts. v3.16.3 — alert hiding scoped to actual
   * UI affordances rather than all `.alert-*` instances; some
   * info alerts (e.g. session description on the participant
   * journey page) ARE the printable content.
   */
  nav,
  .sf-sidebar,
  .btn,
  .btn-group,
  .navbar,
  /* Toasts and dismissible alerts that are pure UI chrome —
   * NOT all .alert-info instances (those can carry session
   * descriptions on participant pages). The .alert-dismissible
   * class is the marker for "this alert has a close button",
   * which means it's an interaction-flow message, not content. */
  .alert.alert-dismissible,
  .toast-container,
  footer,
  [data-tag-autocomplete] + #tag-suggestions,
  /* Editor sidebar/toolbar/preview — only the rendered content
   * matters on paper. The preview pane in exercise-edit.ejs is
   * the same content that the visual editor produces, so the
   * editor itself is redundant when you have the preview. */
  #editor-toolbar,
  #json-editor,
  .sticky-lg-top .card-header,
  /* Filter chips on the library list */
  [aria-label="Filter by tag"] {
    display: none !important;
  }
  body {
    background: white;
    color: #000;
    font-size: 11pt;
  }
  main {
    padding: 0 !important;
    max-width: 100% !important;
  }
  /* Cards and columns that were sized for screen become full-width
   * and lose their borders so the printed output reads as a clean
   * document rather than a series of boxed panels. */
  .col-lg-5,
  .col-lg-7,
  .col-md-6,
  .col-12 {
    width: 100% !important;
    max-width: 100% !important;
    flex: 0 0 100% !important;
  }
  .card {
    border: none !important;
    background: white !important;
    box-shadow: none !important;
  }
  /* Each element block (instruction, interact, etc.) prints as a
   * coherent unit; avoid breaking across pages mid-element. */
  .element-block,
  .role-block,
  #editor-preview > * {
    page-break-inside: avoid;
    break-inside: avoid;
  }
  /* Sticky-positioned containers don't make sense on paper. */
  .sticky-top,
  .sticky-lg-top {
    position: static !important;
  }
  /* Hyperlinks: show the URL in parentheses for paper reference,
   * unless it's an internal-anchor or app-relative link (those
   * mean nothing on paper). */
  a[href^="http"]:after {
    content: " (" attr(href) ")";
    font-size: 9pt;
    color: #555;
  }
}

/*
 * v3.11.0 — newly-activated challenge flash. When the participant's
 * polling reveals a new active challenge, the card briefly highlights
 * to draw attention. CSS-only animation; participant-journey.js adds
 * the class and removes it after ~3 seconds.
 */
.new-content-flash {
  animation: sf-new-content-flash 3s ease-out;
}
@keyframes sf-new-content-flash {
  0%   { box-shadow: 0 0 0 0 rgba(13, 165, 242, 0.6); background-color: rgba(13, 165, 242, 0.08); }
  10%  { box-shadow: 0 0 0 8px rgba(13, 165, 242, 0.3); background-color: rgba(13, 165, 242, 0.12); }
  100% { box-shadow: 0 0 0 0 rgba(13, 165, 242, 0); background-color: transparent; }
}

/* Reduce motion: respect user's preference. The flash still happens but as
 * a single fade, no pulsing box-shadow. */
@media (prefers-reduced-motion: reduce) {
  .new-content-flash {
    animation: sf-new-content-fade 1.5s ease-out;
  }
  @keyframes sf-new-content-fade {
    0%   { background-color: rgba(13, 165, 242, 0.15); }
    100% { background-color: transparent; }
  }
}

/*
 * v3.11.0 — countdown banner urgency styling. The challenge.js timer
 * swaps .alert-info → .alert-warning at the 30-second mark; the
 * .deadline-passed class hits at 0. Just colour transitions; the
 * actual state lives in JS.
 */
.alert.deadline-passed {
  border-color: var(--bs-warning);
  background-color: rgba(var(--bs-warning-rgb), 0.15);
}

/*
 * v3.36.0 — reveal panel collapse chevron.
 * The "Reveal panel · N / M elements shown" header is a Bootstrap-
 * collapse toggle; aria-expanded reflects the state. We rotate the
 * chevron-up icon 180deg when the panel is collapsed so it looks
 * like a chevron-down (convention: down = "click to expand").
 */
.reveal-panel-chevron {
  transition: transform 0.2s ease;
}
button[aria-expanded="false"] .reveal-panel-chevron {
  transform: rotate(180deg);
}

/* v8.0.2 — live reveal-panel rows: bigger, clearer touch targets.
 * Each row carries the activate checkbox, the 💬 thread cluster (opens the
 * responses modal), and the ⓘ preview. They were p-0 glyphs ~8px apart inside
 * the row label — too small, too close, and with no visible edge between them.
 * Give each interactive control a ≥44px hit area, raise the row height, and
 * separate + reveal the two right-hand buttons on hover/focus. The buttons
 * already preventDefault, so a larger hit area won't toggle the checkbox. */
.reveal-row {
  min-height: 44px;
  gap: 0.5rem;
}
.reveal-row [data-reveal-checkbox] {
  width: 1.25rem;
  height: 1.25rem;
  flex-shrink: 0;
}
.reveal-row .reveal-thread-cluster,
.reveal-row .reveal-chart-open,
.reveal-row [data-reveal-preview] {
  /* v12.0.x (item 2) — was 44px. Widened so an element with no responses (just
     the rocket) is the same width as one showing the rocket + a 2-digit count,
     and the chart button matches — so the access-button column reads uniform
     regardless of content. (A rare unread badge / 3-digit count can still extend
     a button past this minimum.) Still >= the 44px min touch target. */
  /* v12.0.192 (item 2e) — was 3.75rem. Widened so a locked element (rocket +
     count + 🔒) is the SAME width as a bare rocket or a rocket + count, so the
     access-button column reads uniform regardless of lock/count. */
  min-width: 4.75rem;
  min-height: 44px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  border-radius: 8px;
  flex-shrink: 0;
}
.reveal-row .reveal-thread-cluster,
.reveal-row .reveal-chart-open { margin-left: 0.25rem; }
.reveal-row [data-reveal-preview] { margin-left: 0.125rem; }
.reveal-row .reveal-thread-cluster:hover,
.reveal-row .reveal-thread-cluster:focus-visible,
.reveal-row [data-reveal-preview]:hover,
.reveal-row [data-reveal-preview]:focus-visible {
  background: rgba(102, 16, 242, 0.12);
  outline: none;
}

/* v12.0.194 (screenshot item 2c) — surface which live elements need the leader,
 * in line with the threads-view's amber "Needs you" (awaiting_leader). A row
 * whose thread is waiting on the leader gets a left amber accent + faint wash;
 * an inline amber flag names it; the header carries an amber count pill. Same
 * amber tokens as the responses-modal Needs-you section (#fff3cd / #7a4f01). */
.reveal-row.reveal-row-needs-leader {
  box-shadow: inset 3px 0 0 var(--bs-warning, #ffc107);
  background: rgba(255, 193, 7, 0.08);
}
/* v15.4.0 (leader batch g) — .reveal-needs-flag retired: the marker moved
   INSIDE the rocket button (it sat in the row <label>, so tapping it toggled
   the show/hide checkbox). Same amber vocabulary, compact icon-only pill,
   no change to the button's height. */
/* v15.4.0 (leader batch h) — the leader's closure note under the
   participant's "Chat closed by leader" notice. */
.sfp-chat-closed-note {
  margin-top: 0.35rem;
  font-size: 0.82rem;
  color: var(--bs-secondary-color, #6c757d);
  font-style: italic;
  white-space: pre-wrap;
}
.reveal-thread-needs {
  color: #7a4f01;
  background: #fff3cd;
  font-size: 0.68rem;
  padding: 0.1rem 0.32rem;
  vertical-align: middle;
}
.reveal-needs-pill {
  display: inline-flex;
  align-items: center;
  gap: 0.25rem;
  padding: 0.05rem 0.5rem;
  font-size: 0.72rem;
  font-weight: 700;
  color: #7a4f01;
  background: #ffe69c;
  border-radius: 999px;
  white-space: nowrap;
}

/*
 * v3.36.0 — design mode: greyed-out style for challenges currently live.
 * The user shouldn't move/delete a live challenge from design mode (live
 * lifecycle owns it).
 *
 * v4.5.4 — Item 5: dropped the opacity dimming. A live challenge is
 * worth HIGHLIGHTING (it's happening right now) not dimming.
 * Light-blue background + clickable Live label are applied via classes
 * on the row in _journey-design-mode.ejs (border-primary-subtle
 * bg-primary-subtle); the interaction lock — drag handle disabled,
 * row-controls neutralised — remains via .challenge-row-controls
 * below so the can't-mutate-while-live guarantee is intact.
 */
.challenge-live-locked .challenge-row-controls {
  pointer-events: none;
}

/*
 * v4.5.5 — Item 7: ± adjust button sizing for the timer modal.
 * The pre-v4.5.5 modal used btn-lg (~48px tall) for all six adjust
 * buttons. Per the UX-panel review:
 *   - "smaller buttons" was an explicit user ask
 *   - WCAG 2.5.5 Target Size (Enhanced) wants ≥44×44 px for touch
 *
 * Compromise: drop the btn-lg class (yielding default ~38px tall)
 * but pin min-height to 44px so touch targets stay adequate. The
 * narrower default horizontal padding lets two columns fit
 * comfortably side-by-side; the explicit min-height keeps mobile
 * thumbs happy.
 *
 * font-weight bumped so the +/- glyph reads clearly inside the
 * smaller button. Icons (bi-plus-lg / bi-dash-lg) provide the
 * third affordance channel called out in the accessibility
 * review (colour + sign + icon — survives any one failing).
 */
.sf-timer-adjust-btn {
  min-height: 44px;
  font-weight: 600;
  display: inline-flex;
  align-items: center;
  justify-content: center;
}

/*
 * v3.36.0 — Sortable.js drag styles for design-mode session + challenge
 * lists. .sortable-ghost is the placeholder, .sortable-chosen is the
 * grabbed item, .sortable-drag is the floating clone.
 */
.session-block.sortable-ghost,
.challenge-design-row.sortable-ghost,
[data-sortable-challenges] [data-challenge-id].sortable-ghost {
  opacity: 0.4;
  background-color: var(--bs-warning-bg-subtle, #fff3cd);
}
.session-block.sortable-chosen,
.challenge-design-row.sortable-chosen,
[data-sortable-challenges] [data-challenge-id].sortable-chosen {
  cursor: grabbing;
}
.session-drag-handle,
.challenge-drag-handle {
  cursor: grab;
  user-select: none;
}
.session-drag-handle:active,
.challenge-drag-handle:active {
  cursor: grabbing;
}

/*
 * v3.36.1 — focus indicator for the keyboard-accessible session drag
 * handle. Sortable handles are mouse-only by default; v3.36.1 added
 * tabindex+role=button to make them focusable and arrow-key reorderable.
 * The handle needs a clear focus outline to communicate it's an
 * interactive control.
 */
.session-drag-handle:focus-visible {
  outline: 2px solid var(--bs-primary);
  outline-offset: 2px;
  border-radius: 3px;
}

/*
 * v3.37.0 — Live mode tab glow.
 * The mode switcher buttons (Design / Prepare / Live) are styled the
 * same when inactive. When a challenge is live, the Live mode button
 * gets a `mode-tab-is-live` class (server-side, based on hasLive) so
 * users can see the journey is broadcasting without switching tabs.
 * Replaces the earlier in-row "Live" pill badge approach.
 *
 * v4.5.1 — when has-live, the Live tab uses a solid primary-blue fill
 * with white text and icon, matching the live-page header colour. The
 * pulsing glow stays, but now it sits on a filled button instead of
 * transparent (which looked like "blue text floating" when the tab
 * was inactive — the user couldn't tell that Live was the destination
 * to click to manage the running session).
 */
.mode-tab-is-live {
  font-weight: 600;
  /* Subtle "pulsing" animation so the glow draws the eye without
     being shouty. 2s cycle keeps it gentle. The glow is OUTSIDE the
     button (box-shadow) so it doesn't fight with the white text. */
  animation: sf-mode-tab-pulse 2s ease-in-out infinite;
}
@keyframes sf-mode-tab-pulse {
  0%, 100% { box-shadow: 0 0 6px rgba(var(--bs-primary-rgb), 0.55); }
  50%      { box-shadow: 0 0 12px rgba(var(--bs-primary-rgb), 0.85); }
}
@media (prefers-reduced-motion: reduce) {
  .mode-tab-is-live {
    animation: none;
  }
}

/* v4.5.1 — when the Live tab is INACTIVE (user is on Design or
 * Prepare) but has-live is true, override the transparent inactive
 * styling so the tab stays solid-blue with white text. The button
 * also stays interactive: clicking it switches to live mode. */
#mode-switcher .btn.mode-tab-inactive.btn-primary.mode-tab-is-live {
  /* v8.3.4 — #3a: inactive-but-something-live = blue OUTLINE + pulse, NOT a
     solid fill. The old v4.5.1 rule filled it solid blue, which made the Live
     tab look identical to its active state — so it appeared "always filled
     blue" whenever a challenge was live. The pulse (from .mode-tab-is-live)
     still draws the eye to it as the destination; solid blue is now reserved
     for the active state (the .active rule) only. */
  background-color: transparent;
  color: var(--bs-primary);
  border-color: var(--bs-primary);
}
#mode-switcher .btn.mode-tab-inactive.btn-primary.mode-tab-is-live .bi {
  color: var(--bs-primary);
}

/* ===== Participant waiting state ===== */
.participant-waiting-pulse {
  display: inline-block;
  width: 8px; height: 8px;
  border-radius: 50%;
  background: var(--bs-secondary-color);
  animation: sf-pulse 1.8s ease-in-out infinite;
}
@keyframes sf-pulse {
  0%, 100% { opacity: 0.3; transform: scale(0.85); }
  50%       { opacity: 1;   transform: scale(1.1);  }
}

/* Session details: hide default arrow, rely on our chevron */
details.mb-3 > summary { list-style: none; }
details.mb-3 > summary::-webkit-details-marker { display: none; }

/* ===== details[open] chevron rotation ===== */
details[open] > summary .details-chevron {
  transform: rotate(180deg);
}
summary .details-chevron {
  transition: transform 0.2s ease;
}

/* ===== Participant challenge cards ===== */
/* Active challenge card — slightly elevated to stand out */
.challenge-list-active {
  box-shadow: 0 0 0 2px var(--bs-success-border-subtle);
}

/* =============================================================================
 * v3.42.0 — PARTICIPANT UI OVERHAUL
 * Ground-up redesign of the participant challenge experience.
 * All rules scoped to .sf-p (participant body class) or specific selectors
 * to avoid touching leader styles.
 * =========================================================================== */

/* ── Participant body class ── */
body.sf-p {
  --sfp-brand:          #0DA5F2;
  --sfp-brand-light:    rgba(13,165,242,0.08);
  --sfp-brand-mid:      rgba(13,165,242,0.16);
  --sfp-answered:       #198754;
  --sfp-answered-light: rgba(25,135,84,0.08);
  /* v12.0.45/47 (#8/#9) — participant accent. Bootstrap $indigo-400 (#8540f5);
     white text on it is ~5.2:1, clearing WCAG AA (4.5:1). Hover/active is the
     next step down, $indigo-500 (#6610f2). */
  --sfp-indigo:         #8540f5;
  --sfp-indigo-dark:    #6610f2;
  /* v12.0.54 — soft indigo tint for interactive-element option hover/selected
     fills (parallels --sfp-brand-light), so a question's options match its
     indigo header + submit button instead of the brand-blue they used before. */
  --sfp-indigo-light:   rgba(133,64,245,0.10);
  --sfp-radius:         14px;
  --sfp-radius-sm:      10px;
  --sfp-shadow:         0 1px 4px rgba(0,0,0,0.06);
  --sfp-shadow-md:      0 4px 16px rgba(0,0,0,0.10);
  background: #f5f6f8;
}

/* ── Participant nav ── */
.sfp-nav {
  background: #fff;
  border-bottom: 1px solid #e8eaed;
  box-shadow: 0 1px 3px rgba(0,0,0,0.06);
}
.sfp-nav .sfp-nav-title {
  font-size: 0.8rem;
  color: var(--bs-body-secondary-color, #6c757d);
  line-height: 1.2;
  max-width: 200px;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* v5.2.2 — item 1.14a. Hide the top "Time remaining" countdown entirely
 * when time's up. Pre-v5.2.2 the JS painted the label to "Time's up"
 * but the countdown row kept its slot in the layout — the user
 * reported it as redundant since the big "Time's up" banner below
 * carries the same meaning much more clearly.
 *
 * Mechanism: challenge.js adds .expired to .sfp-countdown when
 * remaining hits zero and removes it on time extension; the CSS rule
 * here re-uses that flag for visibility. No JS change required for
 * the re-show case — removing the class restores display:flex.
 *
 * !important: there's no Bootstrap utility on .sfp-countdown that
 * would override display, but the flex container layout means we
 * need display:none, not visibility:hidden — otherwise the empty
 * slot still pushes other elements down. */
.sfp-countdown.expired {
  display: none !important;
}

/* ── Challenge page main layout ── */
.sfp-page {
  max-width: 680px;
  margin: 0 auto;
  padding: 1.5rem 1rem 4rem;
  /* v3.55.1 — defensive overflow guard. User reported multi-role
   * content blocks "extend to full width" past the .sfp-page's
   * 680px constraint after initial load. The most common cause for
   * this pattern is a child element with intrinsic width larger
   * than the parent (a wide image, a pre-wrapped table, a markdown
   * embed with explicit min-width, etc.); without overflow-x:
   * hidden the child pushes the page wider than its declared
   * max-width. Combined with explicit max-width: 100% on every
   * direct child block below, this keeps the layout reliably
   * inside the 680px column. */
  overflow-x: hidden;
}
/* v5.2.2 — item 1.10. Wider variant for the journey lobby page.
 * Pre-v5.2.2 the lobby grid (.sfp-lobby-grid) had media queries at
 * 720px / 1100px that switched to 2 / 3 columns — but those rules
 * read the VIEWPORT width while the GRID was constrained to
 * .sfp-page's 680px max-width. Net result: even on a 1440px screen
 * the cards stayed single-column because the parent had no room.
 * The --wide variant lifts the cap to 1100px so the breakpoints
 * actually fire. The challenge-detail page intentionally STAYS at
 * 680px (long-form reading column, narrow is better there). */
.sfp-page--wide {
  max-width: 1100px;
}
.sfp-page > * {
  max-width: 100%;
  box-sizing: border-box;
}
.sfp-page img,
.sfp-page video,
.sfp-page iframe,
.sfp-page table,
.sfp-page pre {
  max-width: 100%;
  height: auto;
}
@media (min-width: 576px) {
  .sfp-page { padding: 2rem 1.5rem 4rem; }
}

/* ── Challenge header ── */
.sfp-challenge-title {
  font-size: 1.5rem;
  font-weight: 700;
  line-height: 1.25;
  color: #1a1d21;
  margin-bottom: 0.25rem;
}
@media (min-width: 576px) {
  .sfp-challenge-title { font-size: 1.75rem; }
}

/* ── Progress bar ── */
.sfp-progress {
  background: #e8eaed;
  border-radius: 100px;
  height: 5px;
  margin-bottom: 1.5rem;
  overflow: hidden;
}
.sfp-progress-fill {
  height: 100%;
  background: var(--sfp-answered);
  border-radius: 100px;
  transition: width 0.4s ease;
}
.sfp-progress-label {
  font-size: 0.78rem;
  color: var(--bs-body-secondary-color, #6c757d);
  margin-bottom: 0.4rem;
}

/* ── Content elements (text, markdown, image, link) ── */
.sfp-content {
  background: #fff;
  border-radius: var(--sfp-radius);
  box-shadow: var(--sfp-shadow);
  margin-bottom: 1rem;
  overflow: hidden;
}
.sfp-content-body {
  padding: 1.25rem 1.25rem 1rem;
}
@media (min-width: 576px) {
  .sfp-content-body { padding: 1.5rem; }
}
.sfp-content-heading {
  font-size: 1rem;
  font-weight: 700;
  color: #1a1d21;
  margin-bottom: 0.625rem;
  padding-bottom: 0.5rem;
  border-bottom: 2px solid #f0f1f3;
}
.sfp-content p { color: #3d4047; line-height: 1.7; margin-bottom: 0.75rem; }
.sfp-content p:last-child { margin-bottom: 0; }
.sfp-content .markdown-content { color: #3d4047; line-height: 1.7; }
.sfp-content .markdown-content h4 { font-size: 1.1rem; font-weight: 700; margin-top: 1rem; }
.sfp-content .markdown-content h5 { font-size: 0.95rem; font-weight: 700; margin-top: 0.75rem; }
.sfp-content .markdown-content blockquote {
  border-left: 3px solid var(--sfp-brand); padding-left: 1rem; color: #6c757d; margin: 0.75rem 0;
}

/* ── v14.58.0 (user report 7) — the NEW-element affordance ──
   Elements revealed after the participant's last hard load get a subtle
   cream tint + a small "New" pill (top-right). One consistent treatment for
   every element kind — quieter than per-type variants, and the pill names
   the reason for the tint. Applied by challenge.js (markNewElements);
   cleared by any hard reload (the class is never SSR'd). */
[data-element-id].sfp-el-new {
  position: relative;
  background: #fffcf2;                       /* light cream, one step off the card white */
  box-shadow: 0 0 0 1px rgba(212, 175, 55, 0.30); /* soft amber hairline, reads as "glow" on bordered kinds */
  border-radius: var(--sfp-radius);
  /* v14.59.0 (report 6) — the pill used to straddle the top edge at
     top:-8px and got CLIPPED: card elements establish a rounded overflow
     context. Keep the badge INSIDE the box instead of fighting the clip
     (overflow:visible would break the rounded corners of image/table
     children), and pad the top so it never sits on the heading. */
  padding-top: 0.85rem;
}
.sfp-el-new-badge {
  position: absolute;
  top: 8px;
  right: 10px;
  background: #d4af37;
  color: #fff;
  font-size: 0.62rem;
  font-weight: 700;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  padding: 2px 7px;
  border-radius: 999px;
  box-shadow: 0 1px 2px rgba(20, 30, 50, 0.18);
  pointer-events: none;
}

/* ── Image element ── */
/* v14.57.2 (IMG.2) — ratio-aware centred sizing. The wrap (not the img)
   carries the width so the corner zoom button positions against the visible
   image, and everything centres via margin:auto. The load handler
   (challenge.js / preview.js / img-ratio.js) reads naturalWidth/Height and
   adds sfp-img-portrait (~50%) or sfp-img-square (~75%); landscape stays the
   default full width, inset by the card's common padding. */
.sfp-img-wrap {
  --sfp-img-pad: 1.25rem;
  position: relative;
  border-radius: var(--sfp-radius);
  overflow: hidden;
  width: calc(100% - (var(--sfp-img-pad) * 2)); /* landscape: full width minus common padding */
  margin: var(--sfp-img-pad) auto;              /* horizontally centred */
}
@media (min-width: 576px) {
  .sfp-img-wrap { --sfp-img-pad: 1.5rem; }      /* tracks .sfp-content-body padding */
}
.sfp-img-wrap.sfp-img-portrait { width: 50%; }
.sfp-img-wrap.sfp-img-square   { width: 75%; }
/* No margin doubling when the caption follows directly. */
.sfp-img-wrap + .sfp-img-caption { margin-top: calc(var(--sfp-img-pad, 1.25rem) * -0.5); }
.sfp-img-wrap img {
  display: block; width: 100%; height: auto;
  transition: transform 0.2s;
}
.sfp-img-wrap:hover img { transform: scale(1.01); }
/* v14.57.2 (IMG.2) — the "Tap to enlarge" pill is a small round zoom icon in
   the image's corner; the whole image stays the primary tap target
   (data-action="open-zoom" on the img itself), the button is the visible
   affordance + a second, keyboard-reachable trigger. Always visible: the
   participant surface is mobile-first, hover-reveal hides it exactly where
   it's needed. */
.sfp-img-zoom-btn {
  position: absolute; bottom: 8px; right: 8px;
  width: 34px; height: 34px;
  display: flex; align-items: center; justify-content: center;
  border: 0; border-radius: 50%;
  background: rgba(0, 0, 0, 0.45); color: #fff;
  font-size: 1rem; line-height: 1;
  cursor: zoom-in;
  transition: background 0.2s;
}
.sfp-img-zoom-btn:hover,
.sfp-img-zoom-btn:focus-visible { background: rgba(0, 0, 0, 0.7); }
.sfp-img-caption {
  width: 75%;
  margin: 0 auto;
  padding: 0 0 1rem;
  font-size: 0.82rem;
  color: #6c757d;
  text-align: center;
}

/* ── Table element (v12.0.105) ── */
.sf-el-table .table { margin-bottom: 0; }
/* CR.1 — with table-layout:fixed on the editor preview, wrap long cell content
   inside its column (and top-align) instead of forcing the column wider. */
.sf-tbl-preview-fixed th, .sf-tbl-preview-fixed td { overflow-wrap: anywhere; vertical-align: top; }
.sfp-table-caption {
  font-size: 0.82rem;
  color: #6c757d;
}
/* v12.0.106 — opt-in thumbnail: a capped preview that expands on tap. CSS-only
   (a hidden checkbox toggled by the label), so it needs no JS and works in every
   render context that ships this markup + stylesheet. */
/* PT.2 — a collapsed table is a compact GREEN bar (the table accent colour),
   not a clipped 340x220 preview that crowds the participant view. The bar
   expands to the full table on tap. */
.sfp-table-collapsible { display: none; }
.sfp-table-toggle:checked ~ .sfp-table-collapsible {
  display: block;
  overflow-x: auto;                   /* full table + horizontal scroll when expanded */
}
.sfp-table-toggle-btn {
  display: flex;
  align-items: center;
  gap: 0.45rem;
  margin-top: 0;
  cursor: pointer;
  user-select: none;
  font-size: 0.9rem;
  font-weight: 600;
  padding: 0.55rem 0.85rem;
  border-radius: 0.5rem;
  color: var(--bs-green);
  background: #e9f7ef; /* fallback */
  background: color-mix(in srgb, var(--bs-green) 12%, var(--bs-body-bg));
  border: 1px solid #b7e4c7; /* fallback */
  border: 1px solid color-mix(in srgb, var(--bs-green) 40%, var(--bs-body-bg));
}
.sfp-table-toggle-title { flex: 1 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* v12.0.307 (feedback L4) — the tiny ::after text caret is replaced by a
   real bootstrap-icons chevron in the markup, sized/weighted to match the
   interact card's .sfp-chat-toggle-chevron so tables and question headers
   share one collapse vocabulary. */
.sfp-table-toggle-chevron {
  margin-left: auto;
  flex-shrink: 0;
  font-size: 1.05rem;
  font-weight: 700;
  color: #6c757d;
  transition: transform 0.15s ease;
}
.sfp-table-toggle:checked ~ .sfp-table-toggle-btn .sfp-table-toggle-chevron { transform: rotate(180deg); }
/* v12.0.117 — the toggle checkbox is visually-hidden (not display:none) so it
   stays keyboard-focusable; surface a focus ring on the visible label. */
.sfp-table-toggle:focus-visible ~ .sfp-table-toggle-btn {
  outline: 2px solid var(--bs-primary);
  outline-offset: 2px;
  border-radius: 2px;
}

/* ── Link element ── */
.sfp-link-card {
  display: flex;
  align-items: center;
  gap: 1rem;
  padding: 1rem 1.25rem;
  text-decoration: none;
  color: inherit;
  transition: background 0.15s;
}
.sfp-link-card:hover { background: #f8f9fa; color: inherit; }
.sfp-link-icon {
  width: 40px; height: 40px; flex-shrink: 0;
  background: var(--sfp-brand-light);
  border-radius: 10px;
  display: flex; align-items: center; justify-content: center;
  color: var(--sfp-brand); font-size: 1rem;
}
.sfp-link-text { flex: 1; min-width: 0; }
.sfp-link-label { font-weight: 600; font-size: 0.9rem; color: #1a1d21; }
.sfp-link-domain { font-size: 0.75rem; color: #8e9399; margin-top: 2px; }
.sfp-link-arrow { color: #b0b7bf; font-size: 1rem; flex-shrink: 0; }

/* ── Interact / question elements ── */
.sfp-question {
  background: #fff;
  border-radius: var(--sfp-radius);
  box-shadow: var(--sfp-shadow);
  margin-bottom: 1rem;
  overflow: hidden;
  border: 2px solid transparent;
  transition: border-color 0.2s, box-shadow 0.2s;
}
.sfp-question:focus-within {
  border-color: var(--sfp-brand);
  box-shadow: 0 0 0 3px var(--sfp-brand-light), var(--sfp-shadow);
}
.sfp-question.is-answered {
  border-color: var(--sfp-answered);
}
/* "+ extra rounds": which round the participant is answering (shown round 2+). */
.sfp-round-badge {
  /* item 7 — light indigo when collapsed (was light blue), tying the round
     marker to the question's indigo identity. */
  background: var(--sfp-indigo-light, rgba(133,64,245,0.10));
  color: var(--sfp-indigo-dark, #6610f2);
  font-size: 0.72rem;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  padding: 0.35rem 1.25rem;
  border-bottom: 1px solid rgba(102, 16, 242, 0.18);
}
/* item 7 — full indigo once the question is expanded (open). */
.sfp-chat.is-expanded .sfp-round-badge {
  background: var(--sfp-indigo);
  color: #fff;
  border-bottom-color: var(--sfp-indigo-dark);
}
.sfp-question-header {
  padding: 1rem 1.25rem 0;
  display: flex;
  justify-content: space-between;
  align-items: flex-start;
  gap: 0.75rem;
}
@media (min-width: 576px) {
  .sfp-question-header { padding: 1.25rem 1.5rem 0; }
}
.sfp-question-prompt {
  font-size: 1rem;
  font-weight: 600;
  color: #1a1d21;
  line-height: 1.5;
  flex: 1;
}
.sfp-answered-badge {
  flex-shrink: 0;
  display: inline-flex;
  align-items: center;
  gap: 4px;
  font-size: 0.72rem;
  font-weight: 600;
  color: var(--sfp-answered);
  background: var(--sfp-answered-light);
  border-radius: 20px;
  padding: 3px 10px;
  white-space: nowrap;
}
.sfp-question-body {
  padding: 0.875rem 1.25rem 1.25rem;
}
@media (min-width: 576px) {
  .sfp-question-body { padding: 1rem 1.5rem 1.5rem; }
}

/* ── Choice cards (radio options) ── */
.sfp-choices { display: flex; flex-direction: column; gap: 0.5rem; margin-bottom: 1rem; }
.sfp-choice {
  position: relative;
  display: flex;
  align-items: center;
  gap: 0.875rem;
  padding: 0.875rem 1rem;
  border: 2px solid #e3e6ea;
  border-radius: var(--sfp-radius-sm);
  cursor: pointer;
  transition: border-color 0.15s, background 0.15s;
  user-select: none;
  -webkit-tap-highlight-color: transparent;
}
.sfp-choice:hover {
  border-color: var(--sfp-indigo);
  background: var(--sfp-indigo-light);
}
.sfp-choice input[type="radio"] {
  width: 20px; height: 20px;
  flex-shrink: 0;
  accent-color: var(--sfp-indigo);
  cursor: pointer;
}
.sfp-choice-label {
  font-size: 0.95rem;
  color: #3d4047;
  line-height: 1.45;
  cursor: pointer;
  flex: 1;
}
.sfp-choice.is-selected {
  border-color: var(--sfp-indigo);
  background: var(--sfp-indigo-light);
}
.sfp-choice.is-selected .sfp-choice-label { color: var(--sfp-indigo-dark); font-weight: 500; }
/* v12.0.58 — answered (locked) state. The chosen option stays indigo and at full
   strength; the others mute to light grey so "what was picked" reads at a glance.
   Replaces the earlier green answered colour + the blanket fade, and is mirrored on
   the image cards below. */
.sfp-choice[data-disabled="true"] {
  cursor: not-allowed;
  pointer-events: none;
}
.sfp-choice[data-disabled="true"].is-selected {
  border-color: var(--sfp-indigo);
  background: var(--sfp-indigo-light);
}
.sfp-choice[data-disabled="true"].is-selected .sfp-choice-label {
  color: var(--sfp-indigo-dark); font-weight: 500;
}
.sfp-choice[data-disabled="true"]:not(.is-selected) {
  border-color: #e6e7ea;
  background: #f5f6f7;
}
.sfp-choice[data-disabled="true"]:not(.is-selected) .sfp-choice-label {
  color: #9aa0a6;
}

/* ── v12.0.11 (DEMO.5) — option/chat image thumbnails carry a visible
   enlarge badge so it's obvious they tap to open fullscreen (with caption).
   The badge is decorative (pointer-events:none) — the image itself is the
   open-zoom target. ── */
/* v12.0.29 (item 1a) — thumbnail now sits at the RIGHT edge of the option row.
   The label has flex:1 and pushes this (flex-shrink:0) wrap to the right; the
   row's gap provides the spacing. Previously it sat between the radio and the
   label (margin-right) and overlapped the selection tap target. */
.sfp-choice-image-wrap { position: relative; display: inline-block; flex-shrink: 0; line-height: 0; }
.sfp-choice-image-zoom,
.sfp-chat-choice-zoom {
  position: absolute; bottom: 2px; right: 2px;
  display: inline-flex; align-items: center; justify-content: center;
  background: rgba(0,0,0,0.6); color: #fff;
  border-radius: 3px; font-size: 0.6rem; line-height: 1; padding: 2px 3px;
  pointer-events: none;
}
.sfp-chat-choice-thumb-wrap { position: relative; display: inline-block; vertical-align: middle; line-height: 0; }

/* ── v12.0.33 (item 1) — choice density. Image options render as a selectable
   CARD GRID (the whole card is the target; a corner button enlarges) instead of
   a list with a tiny thumbnail. Long lists (>12) gain a type-to-filter that
   narrows in place. Lists flow in the page — no inner scroll box — so nothing
   hides below a false bottom; the filter is the shortener. ── */
.sfp-choice-filterbar { display: flex; align-items: center; gap: 0.625rem; margin-bottom: 0.625rem; }
.sfp-choice-filter {
  flex: 1; min-width: 0; max-width: 22rem;
  padding: 0.5rem 0.75rem;
  border: 2px solid #e3e6ea; border-radius: var(--sfp-radius-sm);
  font-size: 0.95rem; color: #3d4047; background: #fff;
}
.sfp-choice-filter:focus { outline: none; border-color: var(--sfp-indigo); }
.sfp-choice-count { font-size: 0.8rem; color: #6b7280; white-space: nowrap; }
.sfp-choice-nomatch { font-size: 0.9rem; color: #6b7280; font-style: italic; margin: 0.25rem 0 1rem; }
/* v12.0.283 (review) — the filter marks ANY option shape; hide by the shared
   attribute so new shapes (the v279 compact rows) can never drift out again. */
.sfp-choice.is-hidden, .sfp-choice-card.is-hidden, [data-sfp-choice-opt].is-hidden { display: none; }

/* ── v12.0.35 (item 1) — categories. Options that carry a `group` label render
   in visible labelled sections (not a collapsed accordion), so the structure is
   scannable at a glance. The filter hides a whole section when all its options
   are filtered out. ── */
.sfp-choice-category { margin-bottom: 1.25rem; }
.sfp-choice-category.is-hidden { display: none; }
/* v15.1.0 (user report 2) — category headings are a light-yellow band with
   bold text, matching the leader picker's .responses-picker-category-head
   (responses-modal.js) so the grouping reads the same on both sides.
   #fff3cd / #664d03 = Bootstrap warning-subtle bg/emphasis pair. */
.sfp-choice-category-head {
  font-size: 0.8rem; font-weight: 700; letter-spacing: 0.02em;
  background: #fff3cd; color: #664d03;
  margin: 0 0 0.5rem; padding: 0.2rem 0.5rem; border-radius: 4px;
}

.sfp-choice-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
  gap: 0.75rem;
  margin-bottom: 1rem;
}
.sfp-choice-card {
  position: relative;
  display: flex; flex-direction: column;
  border: 2px solid #e3e6ea; border-radius: var(--sfp-radius-sm);
  overflow: hidden; cursor: pointer; background: #fff;
  transition: border-color 0.15s, box-shadow 0.15s;
  user-select: none; -webkit-tap-highlight-color: transparent;
}
.sfp-choice-card:hover { border-color: var(--sfp-indigo); box-shadow: 0 4px 14px rgba(133,64,245,0.16); }
.sfp-choice-card input { position: absolute; opacity: 0; width: 1px; height: 1px; pointer-events: none; }
/* v14.59.1 (report 4) — CSS-ONLY option-image sizing.
   v14.59.0 tried to do this with a load-time classifier adding
   .sfp-optimg-portrait/-square to the media box — but option images carry
   no `data-image-handler`, which is the attribute attachImageHandlers
   queries, so that code NEVER RAN and the box stayed a hard 4/3. A portrait
   option (the demo 1080x1920) therefore rendered wrong.
   No JS now: the box grows with its image (natural ratio) between a floor
   and a ceiling, and `contain` guarantees the whole image is visible — a
   portrait renders tall-but-capped and centred, never cropped, never a
   thumbnail-sized strip. Landscape and square are unaffected in practice. */
.sfp-choice-card-media {
  position: relative; width: 100%;
  min-height: 120px;
  background: #f1f3f5; line-height: 0; overflow: hidden;
  display: flex; align-items: center; justify-content: center;
}
/* The image sizes ITSELF (max-width + max-height, auto dimensions) and the
   flex box grows to fit. A stretched 100%-wide element with `contain` would
   letterbox a portrait into a thin centred strip — the very defect this
   replaces — because the ELEMENT stays card-wide while only the drawn
   pixels shrink. Letting the element take the image's own shape means a
   portrait renders as wide as its capped height allows (~2/3 of the card),
   not ~1/3. The 420px ceiling keeps six-option grids on one screen. */
.sfp-choice-card-img {
  position: static;
  width: auto; height: auto;
  max-width: 100%; max-height: 420px;
  object-fit: contain; display: block;
}
.sfp-choice-card-zoom {
  position: absolute; top: 6px; right: 6px; z-index: 2;
  width: 26px; height: 26px; padding: 0;
  display: inline-flex; align-items: center; justify-content: center;
  border: none; border-radius: 50%;
  background: rgba(0,0,0,0.55); color: #fff; font-size: 0.7rem; cursor: zoom-in;
}
.sfp-choice-card-zoom:hover { background: rgba(0,0,0,0.78); }
.sfp-choice-card-label { padding: 0.5rem 0.625rem; font-size: 0.9rem; color: #3d4047; line-height: 1.3; flex: 1; }
.sfp-choice-card-check {
  position: absolute; top: 6px; left: 6px;
  width: 24px; height: 24px; border-radius: 50%;
  display: inline-flex; align-items: center; justify-content: center;
  background: #fff; border: 2px solid #e3e6ea; color: transparent; font-size: 0.8rem;
  transition: all 0.12s;
}
.sfp-choice-card.is-selected, .sfp-choice-card:has(input:checked) {
  border-color: var(--sfp-indigo); box-shadow: 0 0 0 2px var(--sfp-indigo) inset;
}
.sfp-choice-card.is-selected .sfp-choice-card-label,
.sfp-choice-card:has(input:checked) .sfp-choice-card-label { color: var(--sfp-indigo-dark); font-weight: 600; }
.sfp-choice-card.is-selected .sfp-choice-card-check,
.sfp-choice-card:has(input:checked) .sfp-choice-card-check {
  background: var(--sfp-indigo); border-color: var(--sfp-indigo); color: #fff;
}
/* v12.0.58 — answered (locked) state, mirroring the text choices: the chosen card
   stays indigo and in full colour (the .is-selected / :has(input:checked) rules
   above still apply); the rest mute to light grey with a desaturated, dimmed image. */
.sfp-choice-card[data-disabled="true"] { cursor: not-allowed; pointer-events: none; }
.sfp-choice-card[data-disabled="true"]:not(.is-selected):not(:has(input:checked)) {
  border-color: #e6e7ea; background: #f5f6f7;
}
.sfp-choice-card[data-disabled="true"]:not(.is-selected):not(:has(input:checked)) .sfp-choice-card-label {
  color: #9aa0a6; font-weight: 400;
}
.sfp-choice-card[data-disabled="true"]:not(.is-selected):not(:has(input:checked)) .sfp-choice-card-img {
  filter: grayscale(0.85); opacity: 0.55;
}

/* ── Textarea input ── */
.sfp-textarea {
  width: 100%;
  /* v8.1.0 — match the chat composer field (.sfp-chat-composer textarea):
     a compact, pill-shaped, single-line-height textarea that auto-grows
     (JS in challenge.js sets the height from scrollHeight on input). Was a
     fixed 4-row / 110px box; now starts at one line like the chat input and
     grows as the response gets longer, then scrolls past max-height. */
  border: 1px solid #ced4da;
  border-radius: 18px;
  padding: 8px 14px;
  font-size: 1rem;        /* 16px — avoids iOS Safari focus auto-zoom */
  line-height: 1.4;
  color: #1a1d21;
  resize: none;
  overflow-y: auto;
  transition: border-color 0.15s, box-shadow 0.15s;
  min-height: 38px;
  max-height: 50vh;
  font-family: inherit;
  background: #fff;
}
.sfp-textarea:focus {
  outline: none;
  border-color: var(--sfp-brand);
  box-shadow: 0 0 0 0.15rem var(--sfp-brand-light);
}
.sfp-char-count {
  font-size: 0.75rem;
  color: #b0b7bf;
  text-align: right;
  margin-top: 4px;
  transition: color 0.2s;
}
.sfp-char-count.near-limit { color: #e67e22; }
.sfp-char-count.at-limit   { color: #dc3545; }

/* ── Submit button ── */
.sfp-submit-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  flex-wrap: wrap;
  gap: 0.5rem;
  margin-top: 0.75rem;
}
.sfp-submit-btn {
  background: var(--sfp-indigo);
  color: #fff;
  border: none;
  border-radius: var(--sfp-radius-sm);
  padding: 0.625rem 1.25rem;
  font-size: 0.9rem;
  font-weight: 600;
  cursor: pointer;
  display: inline-flex;
  align-items: center;
  gap: 0.4rem;
  transition: background 0.15s, transform 0.1s;
  min-height: 44px;
}
.sfp-submit-btn:hover { background: var(--sfp-indigo-dark); }
.sfp-submit-btn:active { transform: scale(0.98); }
.sfp-submit-btn:disabled { opacity: 0.6; cursor: not-allowed; transform: none; }
.sfp-submit-btn.saved {
  background: var(--sfp-answered);
}
.sfp-saved-time {
  font-size: 0.75rem;
  color: #8e9399;
}

/* ── v8.2.0 — strict-alternation composer lock ──
   While awaiting_leader the answer composer is replaced by a quiet
   "waiting for the leader" note; the inputs are disabled (see
   .sfp-choice[data-disabled] / textarea:disabled) and the Save row is
   hidden by [data-sfp-submit-row][hidden]. applyTurnState() in
   challenge.js toggles these live. */
.sfp-answer-waiting {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  margin-top: 0.75rem;
  padding: 0.625rem 0.875rem;
  font-size: 0.85rem;
  color: var(--sfp-brand);
  background: var(--sfp-brand-light);
  border-radius: var(--sfp-radius-sm);
}
.sfp-answer-waiting-icon { font-size: 1rem; flex: 0 0 auto; }
.sfp-answer-waiting[hidden] { display: none; }
/* Gentle cue on the card itself so an answered-but-waiting question reads as
   "in the leader's court" rather than still-editable. */
.sfp-chat.is-awaiting-leader .sfp-answer-card { opacity: 0.92; }
/* v8.3.4 (#7a) — full participant unification: one input at a time. Before the
   participant answers, the answer card IS the composer and the chat composer is
   hidden (answer first). Once answered, the answer card collapses — the answer
   is already surfaced as a bubble in the thread — and the chat composer takes
   over for the turn-based follow-up conversation. */
.sfp-chat.is-answered .sfp-answer-card { display: none; }
/* v18.12.0 (owner 2a/2c) — a POST-IT board is the answer AND the place to keep working (rounds, shared boards): never hidden */
.sfp-chat[data-answer-mode="place"].is-answered .sfp-answer-card { display: block; }
/* 4c — a group-answer thread where ANOTHER member already answered for the group:
   the answer card shows disabled (not hidden) with a one-line note and the group's
   chosen option highlighted in indigo-light, so the member sees the outcome rather
   than an enabled form that only fails on submit. Round-aware (clears on a new
   round). is-answered (the member is the answerer) takes precedence and hides the
   card, so the two states never both apply. */
.sfp-chat.is-group-locked .sfp-answer-card { position: relative; pointer-events: none; opacity: 0.92; }
.sfp-chat.is-group-locked .sfp-answer-card::before {
  content: "Someone in your group already answered for the group.";
  display: block;
  font-size: 0.82rem;
  font-weight: 600;
  color: var(--sfp-indigo-dark);
  background: var(--sfp-indigo-light);
  border: 1px solid var(--sfp-indigo);
  border-radius: 8px;
  padding: 6px 10px;
  margin-bottom: 8px;
}
.sfp-choice-group-picked,
.sfp-choice-card.sfp-choice-group-picked {
  border-color: var(--sfp-indigo) !important;
  background: var(--sfp-indigo-light) !important;
}
.sfp-choice-group-picked .sfp-choice-label,
.sfp-choice-card.sfp-choice-group-picked .sfp-choice-card-label { color: var(--sfp-indigo-dark); font-weight: 600; }
/* v12.0.227 — answer-first: before the participant has answered, the answer
   card IS the call to action, so the composer is suppressed.
   v14.41.0 (user report 3g) — the gate is `has-answered` ("this participant
   has answered in this thread at least once"), NOT `is-answered` ("answered
   the CURRENT round"). On a repeating element setYourTurnState clears
   is-answered every time the leader replies — deliberately, to bring the
   answer OPTIONS back for the next round — and that also re-hid the composer,
   so with "+ extra rounds" on, chat vanished and returned on every round with
   "Allow participant replies" plainly switched on. One class was doing two
   jobs; `has-answered` is added alongside is-answered and never removed by the
   round cycle, so the options still hide per round while the composer stays. */
.sfp-chat:not(.has-answered) .sfp-chat-composer { display: none; }

/* ── Countdown banner ── */
.sfp-countdown {
  background: linear-gradient(135deg, #e8f4fd 0%, #ddf0ff 100%);
  border: 1px solid #b8dff7;
  border-radius: var(--sfp-radius-sm);
  padding: 0.75rem 1rem;
  display: flex;
  align-items: center;
  gap: 0.75rem;
  margin-bottom: 1.25rem;
  font-size: 0.88rem;
}
.sfp-countdown .sfp-countdown-time {
  font-size: 1rem;
  font-weight: 700;
  font-variant-numeric: tabular-nums;
  color: var(--sfp-brand);
}
.sfp-countdown.urgent {
  background: linear-gradient(135deg, #fff3e0 0%, #ffe8cc 100%);
  border-color: #ffb74d;
}
.sfp-countdown.urgent .sfp-countdown-time { color: #e65100; }
.sfp-countdown.expired {
  background: #ffebee;
  border-color: #ef9a9a;
}
.sfp-countdown.expired .sfp-countdown-time { color: #c62828; }

/* v12.0.71 (AN.1a) — adaptive urgency band tints (primary path; `.urgent`
   above is the no-helper fallback). The ring colour is the strong cue; the
   container tint is secondary. Calm = the default blue above. */
.sfp-countdown[data-band="warn"] {
  background: linear-gradient(135deg, #fff3e0 0%, #ffe8cc 100%);
  border-color: #ffb74d;
}
.sfp-countdown[data-band="warn"] .sfp-countdown-time { color: #b46c0a; }
.sfp-countdown[data-band="crit"] {
  background: linear-gradient(135deg, #fdecec 0%, #fcdada 100%);
  border-color: #f1a1a1;
}
.sfp-countdown[data-band="crit"] .sfp-countdown-time { color: #c0292e; }

/* The urgency ring — a shared component (the lobby chip + leader card adopt it
   in AN.1a phase 2). Build-once SVG: a grey track + a band-coloured progress
   arc (rotated so it depletes from 12 o'clock) with a centred value/unit. */
.sfp-countdown-ring { width: 38px; height: 38px; }
/* v14.56.0 (leader reports 4+5) — the challenge-row squeeze, second attempt
   (v14.51.0 fixed the shrink INSIDE the title column; the screenshot shows
   the column itself still starved). The tag badges are inline SIBLINGS of
   the title column in a non-wrapping group, and their nowrap min-content
   wins the shrink war on phones — the title collapses to a one-character
   tower. The left group now wraps (tags drop to their own line) and the
   title column keeps a readable floor. Applies to design, prepare and live
   variants — all three render the same row shape. */
li[data-challenge-id] .d-flex.align-items-center.flex-grow-1.min-w-0 { flex-wrap: wrap; }
li[data-challenge-id] .challenge-display-name { overflow-wrap: anywhere; }
/* v14.57.1 (leader report 2, THIRD round — the real defeat found): the
   title column carried INLINE style="min-width:0" (the v14.51 fix), which
   beats any stylesheet rule — rounds one and two lost a specificity war
   they could not see. The inline style is now a class in all three mode
   partials, and the floor finally holds: the column never shrinks below a
   readable width, the wrapping group drops tags to their own line, and the
   one-character tower (and the tall stretched card it caused) is gone. */
.sf-ch-titlecol { min-width: min(12rem, 60vw); }
li[data-challenge-id] .d-flex.flex-column.flex-grow-1 { min-width: min(12rem, 60vw); }
.sf-ring { position: relative; display: inline-flex; align-items: center; justify-content: center; flex: 0 0 auto; }
.sf-ring-svg { display: block; transform: rotate(-90deg); }
.sf-ring-track { stroke: rgba(0, 0, 0, 0.10); }
.sf-ring-prog { transition: stroke-dashoffset 0.9s linear, stroke 0.3s ease; }
.sf-ring-lab { position: absolute; inset: 0; display: flex; align-items: center; justify-content: center; line-height: 1; font-variant-numeric: tabular-nums; }
.sf-ring-v { font-weight: 700; font-size: 0.72rem; color: var(--bs-body-color, #1f2329); }
.sf-ring-u { font-size: 0.5rem; text-transform: uppercase; letter-spacing: 0.03em; margin-left: 1px; color: var(--bs-secondary-color, #6c757d); }

/* ── Session paused overlay ── */
.sfp-paused-banner {
  /* v12.0.371 (P4) — light yellow (caution), was light indigo */
  background: linear-gradient(135deg, #fff8e1 0%, #fff3cd 100%);
  border: 1px solid #ffe69c;
  border-radius: var(--sfp-radius);
  padding: 1.5rem;
  text-align: center;
  margin-bottom: 1rem;
}

/* ── Role switcher ── */
.sfp-roles {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  flex-wrap: wrap;
  margin-bottom: 1rem;
}

/* v4.2.x — multi-role variant: label gets its own row, pills flow below.
 *
 * Single-role case keeps the inline layout (`.sfp-roles` only) — "Your role: Doctor"
 * reads cleanly on one line at any width.
 *
 * Multi-role case (`.sfp-roles.sfp-roles-multi`) used to share the
 * inline layout, which broke on narrow screens: at 360px viewport
 * the label + 2-3 role pills exceed the .sfp-page's content width
 * (~328px after padding), and the wrap point landed mid-pill or
 * between label and pills inconsistently. Now the label takes a
 * full-width row of its own via flex-basis:100% on the .sfp-roles-label
 * child; the pills wrap naturally on the next row. This gives a
 * consistent layout at every breakpoint AND makes the action
 * affordance clearer ("(click to switch):" labels the row of pills
 * below; v4.3.0 — "click" replaced "tap" since it reads naturally
 * on both desktop and touch devices).
 */
.sfp-roles-multi {
  align-items: flex-start;
  row-gap: 0.5rem;
}
.sfp-roles-multi .sfp-roles-label {
  flex: 1 0 100%;
  margin-bottom: 0;
  line-height: 1.4;
}

.sfp-role-btn {
  border: 2px solid #e3e6ea;
  border-radius: 20px;
  padding: 0.3rem 0.875rem;
  font-size: 0.82rem;
  font-weight: 600;
  cursor: pointer;
  transition: all 0.15s;
  color: #5a6270;
  background: transparent;
  text-decoration: none;
}

/* v4.2.x — slightly tighter pill padding on narrow screens so 2-3 pills
 * have a better chance of fitting on a single row below the label.
 * Saves ~12px per pill (3 pills = ~36px headroom). The visual change
 * is minimal at desktop sizes; meaningful at 320-414px widths. */
@media (max-width: 575.98px) {
  .sfp-role-btn {
    padding: 0.25rem 0.7rem;
    font-size: 0.8rem;
  }
}
.sfp-role-btn.active,
.sfp-role-btn[aria-selected="true"] {
  border-color: var(--sfp-brand);
  background: var(--sfp-brand);
  color: #fff;
}
.sfp-role-btn:hover:not(.active):not([aria-selected="true"]) {
  border-color: var(--sfp-brand);
  color: var(--sfp-brand);
}
/* v5.0.49 — bug 4. Read-only variant. Used by the single-role case
 * where the pill is a display label, not a control. Same brand color
 * styling as .active but cursor:default and no hover behavior. The
 * multi-role case keeps cursor:pointer + hover transitions because
 * clicking a role pill switches the visible role. */
.sfp-role-btn.is-static {
  cursor: default;
}
.sfp-role-btn.is-static:hover {
  /* No hover state change — explicit override of any inherited :hover
   * rule from .sfp-role-btn:hover above. The pill stays as it was. */
  border-color: var(--sfp-brand);
  background: var(--sfp-brand);
  color: #fff;
}

/* ── Journey lobby page ── */

/* v5.0.50 — bug 5.1. Welcome heading area (replaces the verbose
 * info-alert block). Apple-style: large, bold, generous breathing
 * room. */
.sfp-lobby-heading { margin-top: 0.5rem; }
.sfp-lobby-title {
  font-size: clamp(1.75rem, 4vw, 2.5rem);
  font-weight: 700;
  letter-spacing: -0.02em;
  line-height: 1.15;
  margin-bottom: 0.5rem;
}
/* v12.0.395 (participant feedback P4) — THE canonical sub-page title.
 * Every participant sub-page (My progress, Learning, Get my access,
 * Ask your leader, Feedback & issues) uses main.sf-main > .sfp-page,
 * .sfp-back-link, and h1.sfp-title — one look, one size. .sfp-lobby-title
 * stays reserved for the lobby hero alone. Before this, .sfp-title was
 * referenced by progress.ejs but never DEFINED (default h1 size), learning
 * used the lobby hero size, and two pages used h1.h4 — four fonts for one
 * job. */
.sfp-title {
  font-size: clamp(1.35rem, 3vw, 1.65rem);
  font-weight: 700;
  letter-spacing: -0.015em;
  line-height: 1.2;
}

.sfp-lobby-subtitle {
  font-size: 0.95rem;
  line-height: 1.4;
}

/* v5.0.50 — bug 5.2-5.6. Apple-style challenge cards.
 *
 * Pre-v5.0.50 this was a flat horizontal row: name on left, badge on
 * right. User pointed at Apple's product-page cards (large rectangular
 * tiles, subheading on top, big title, visual underneath, chevron-in-
 * circle bottom corner) and asked for the same shape. v5.0.50:
 *
 * Layout:
 *   ┌─────────────────────────────────────┐
 *   │ Session name (sub)         [Live]   │
 *   │ Challenge Title             [3m 12s]│
 *   │                                     │
 *   │     [icon/image area]               │
 *   │                                     │
 *   │                            ⊙→       │
 *   └─────────────────────────────────────┘
 *
 * The visual area in v5.0.50 is a soft-gray placeholder with a
 * default icon (bi-puzzle). Bug 6 will wire this up to a per-
 * exercise image/icon picker from the exercise library.
 *
 * Grid: cards wrap to two-up on >720px, single column on mobile.
 * Aspect ratio: 4:3-ish on mobile, 16:10-ish on desktop — fits both.
 */

/* v5.2.1 — item 9. Lobby card redesign per user feedback:
 *   a. Session + challenge names at TOP (was bottom). Apple-style:
 *      tag/eyebrow above the title; matches the screenshot reference.
 *   b. "Live" status badge REMOVED entirely. The brand-blue accent
 *      bar on .active-challenge already signals activity; the
 *      explicit text badge added noise.
 *   c. Timer chip moves to BOTTOM-LEFT (was top-right with the
 *      Live pill). Keeps the same .sfp-lobby-remaining styling.
 *   d. Responsive grid: 1 col on mobile, 2 col 720+, 3 col 1100+.
 *      Cards have a max-width on small viewports so they can never
 *      become narrower than 4:3 (a card narrower than its image is
 *      tall reads as an awkward strip).
 *   e. Visual area enlarged: min-height 180px (was 130px); icons
 *      4rem (was 3rem); content area is now ABOVE the visual, so
 *      visual takes the remaining card height naturally.
 */
.sfp-lobby-grid {
  display: grid;
  grid-template-columns: 1fr;
  gap: 1rem;
  margin-bottom: 1.5rem;
}
@media (min-width: 720px) {
  .sfp-lobby-grid { grid-template-columns: repeat(2, 1fr); }
}
@media (min-width: 1100px) {
  /* Three-up on wide screens — matches Apple's three-card row in the
   * provided screenshots. */
  .sfp-lobby-grid { grid-template-columns: repeat(3, 1fr); }
}

.sfp-lobby-card {
  position: relative;
  background: #fff;
  border-radius: 1rem;
  box-shadow: var(--sfp-shadow);
  text-decoration: none;
  color: inherit;
  display: flex;
  flex-direction: column;
  overflow: hidden;
  /* v5.2.1 — increased min-height + minimum aspect ratio so a card
   * never becomes narrower than 4:3. The aspect-ratio rule keeps
   * the image area from collapsing on very wide column layouts. */
  min-height: 320px;
  transition: box-shadow 0.2s, transform 0.15s;
  isolation: isolate;
}
.sfp-lobby-card:hover {
  box-shadow: var(--sfp-shadow-md);
  transform: translateY(-2px);
  color: inherit;
}
.sfp-lobby-card:active { transform: translateY(0); }

/* v5.2.6 — item 2a. The pre-v5.2.6 active-challenge state used a
 * 4px brand-blue border-left as the "this is live" cue (the only
 * one left after v5.2.1 removed the explicit Live pill). User
 * feedback: the bar was too visually intrusive against the curated
 * gradients and broke the soft-corner aesthetic. Removed. Active
 * challenges are now distinguished by being non-dimmed (completed
 * challenges keep the .completed-challenge opacity-0.55 rule
 * below) and by their position in the lobby ordering — the active
 * one is rendered first.
 *
 * The .sfp-lobby-card.active-challenge selector is kept in the
 * rendered HTML (participant-journey.js) so a future cue change
 * (e.g. a corner badge, an outline-only ring) has a hook to bind
 * to without touching the JS. */
.sfp-lobby-card.active-challenge {
  /* no visual modifier; reserved selector */
}

/* Completed state: dimmed card. */
.sfp-lobby-card.completed-challenge {
  opacity: 0.55;
}
/* v12.0.x — lobby folders: On-hold and Completed each collapse into a <details>
 * below the live grid, so the participant sees only live (act-now) challenges by
 * default. The native disclosure marker is replaced with a rotating caret; the
 * folder hides entirely when empty (the [hidden] attribute, set by the EJS on load
 * and by participant-journey.js on poll). */
.sfp-lobby-folder { margin-top: 1.25rem; }
.sfp-lobby-folder[hidden] { display: none; }
.sfp-lobby-folder-summary {
  list-style: none;
  cursor: pointer;
  display: flex;
  align-items: center;
  gap: 0.5rem;
  padding: 0.65rem 0.25rem 0.55rem;
  border-top: 1px solid var(--bs-border-color);
  font-size: 0.82rem;
  font-weight: 600;
  color: var(--bs-secondary-color);
  -webkit-user-select: none;
  user-select: none;
}
.sfp-lobby-folder-summary::-webkit-details-marker { display: none; }
/* v12.0.305 (feedback P4) — the folder chevron is GONE: on some browsers it
   rendered on top of the count pill. The row is visibly interactive without
   it (pointer cursor + the count pill); the native disclosure marker is
   suppressed in every engine (::-webkit-details-marker above + ::marker). */
.sfp-lobby-folder-summary::marker { content: ''; }
/* v12.0.326 (feedback, the STILL-unresolved P1) — v305 suppressed the NATIVE
   disclosure marker, but the generic `details > summary::after { content:"›" }`
   rule (the collapsible-card chevron) also matches these folder summaries and
   positions a "›" absolutely at right:0.5rem — directly over the count pill.
   That custom marker is what the user still sees covering the number. Kill it
   for the lobby folders specifically; the row stays visibly interactive
   (pointer cursor + the count pill). */
.sfp-lobby-folder-summary::after { content: none; }
.sfp-lobby-folder-count {
  margin-left: auto;
  font-size: 0.72rem;
  font-weight: 600;
  color: var(--bs-secondary-color);
  background: var(--bs-secondary-bg);
  border-radius: 999px;
  padding: 0.05rem 0.55rem;
}
.sfp-lobby-folder-body { margin-top: 0.5rem; }
.sfp-lobby-caughtup { margin: 0.9rem 0 0.15rem; }

/* v12.0.182 — leader roster live connection dots. Green = connected (heartbeat
   within ~30s), amber = lagging (<120s), red = stale, hollow grey = never seen.
   presence-poll.js rewrites the sfp-conn-* class + title live. */
.sfp-conn-dot {
  display: inline-block;
  width: 9px;
  height: 9px;
  border-radius: 50%;
  flex: 0 0 auto;
  background: var(--bs-secondary-color);
}
.sfp-conn-online { background: var(--bs-success); }
.sfp-conn-idle { background: var(--bs-warning); }
.sfp-conn-stale { background: var(--bs-danger); }
.sfp-conn-none { background: transparent; border: 1px solid var(--bs-border-color); }

/* v12.0.183 — paused session: dim + freeze the live challenge grid. The paused
   banner explains why, and challenge access is already blocked server-side, so the
   live cards go desaturated + non-interactive to read as "held until the leader
   resumes". Folders (on-hold / completed) are unaffected. */
.sfp-lobby-grid.is-session-paused {
  opacity: 0.6;
  filter: grayscale(0.4);
  pointer-events: none;
  transition: opacity 0.2s ease, filter 0.2s ease;
}
/* v12.0.378 (participant feedback 3a) — a paused session hides the On-hold
   and Completed folders entirely: the live grid dims, but the folders stayed
   fully browsable (and their cards clickable) while everything is on hold.
   The paused class lives on the live grid; both folders — the SSR ones and
   the ones participant-journey.js creates — are its FOLLOWING SIBLINGS, so
   one sibling selector covers hard reloads and live pause/resume alike. The
   "all caught up" line goes with them (it invites browsing the folders). */
.sfp-lobby-grid.is-session-paused ~ .sfp-lobby-folder,
.sfp-lobby-grid.is-session-paused ~ .sfp-lobby-caughtup { display: none; }

/* v12.0.185 — the "I need to step away" disclosure is styled as a button; hide the
   native marker triangle so it doesn't overlap the label. */
details.sfp-away-control > summary {
  list-style: none;
  list-style-type: none;
  cursor: pointer;
  display: inline-flex;
  align-items: center;
}
details.sfp-away-control > summary::-webkit-details-marker { display: none; }
details.sfp-away-control > summary::marker { content: ""; font-size: 0; }

/* v12.0.199 (H) — Away-detail popover anchored at an away chip on the roster
   (replaces the toast that appeared far from the chip). Amber, matching the
   away chip family; positioned via inline fixed top/left by presence-poll.js. */
/* v12.0.326 (feedback P2) — the step-away fields sit on the dark sidebar;
   the default .form-control rendered dark-on-dark. Force a light field
   (white bg, dark text) so the inputs are legible, with a clear invalid
   state that shows inside the sidebar (not the native off-canvas bubble). */
.sfp-away-input.form-control {
  background: #fff;
  color: #212529;
  border: 1px solid #ced4da;
}
.sfp-away-input.form-control::placeholder { color: #6c757d; opacity: 1; }
.sfp-away-input.form-control:focus {
  background: #fff;
  color: #212529;
  border-color: var(--sfp-indigo, #8540f5);
  box-shadow: 0 0 0 0.2rem rgba(133, 64, 245, 0.25);
}
.sfp-away-input.form-control.is-invalid {
  border-color: #ffc107;
  box-shadow: 0 0 0 0.2rem rgba(255, 193, 7, 0.25);
}
.sfp-away-detail-pop {
  z-index: 1080;
  width: 224px;
  max-width: calc(100vw - 16px);
  background: #fff3cd;
  border: 1px solid #e3d6a8;
  border-radius: 0.5rem;
  box-shadow: 0 6px 20px rgba(0, 0, 0, 0.18);
  padding: 0.5rem 0.625rem;
  font-size: 0.85rem;
  color: #664d03;
}
.sfp-away-detail-pop[hidden] { display: none; }
.sfp-away-detail-head { font-weight: 600; margin-bottom: 0.25rem; display: flex; align-items: center; }
.sfp-away-detail-row { margin-top: 0.15rem; line-height: 1.35; word-break: break-word; }
.sfp-away-detail-label { display: inline-block; min-width: 3.75rem; color: #8a6d3b; font-size: 0.72rem; text-transform: uppercase; letter-spacing: 0.02em; margin-right: 0.35rem; }
.sfp-away-detail-empty { color: #8a6d3b; font-style: italic; }

/* v5.2.1 — Content block moved TOP (was bottom). Apple-style:
 * session eyebrow + challenge title above the visual. */
.sfp-lobby-card-content {
  padding: 1rem 1.25rem 0.5rem 1.25rem;
  display: flex;
  flex-direction: column;
  gap: 0.25rem;
  flex-shrink: 0;   /* don't compress when card is short */
}
.sfp-lobby-card-subheading {
  font-size: 0.75rem;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  color: var(--bs-secondary-color, #6c757d);
}
.sfp-lobby-card-title {
  font-size: 1.15rem;
  font-weight: 600;
  color: #1a1d21;
  line-height: 1.3;
  letter-spacing: -0.01em;
}

/* v5.2.1 — Visual area now BELOW the content block. Grows to fill
 * remaining card height. Larger icons (4rem) per item 9e. */
.sfp-lobby-card-visual {
  flex: 1 1 auto;
  min-height: 180px;
  background: linear-gradient(135deg, #f1f3f5 0%, #e9ecef 100%);
  display: flex;
  align-items: center;
  justify-content: center;
  color: #adb5bd;
  overflow: hidden;
}
/* v12.0.46 (#5) — an on-hold / not-yet-grouped challenge reads as inactive:
   its visual goes greyscale (no colour) and the "on-hold" label moves from the
   bottom-left status row to a prominent, centred pill over the card. */
.sfp-lobby-card.is-on-hold .sfp-lobby-card-visual { filter: grayscale(1); }
.sfp-lobby-card-hold {
  position: absolute; top: 50%; left: 50%;
  transform: translate(-50%, -50%); z-index: 3;
  background: rgba(73, 80, 87, 0.92); color: #fff;
  font-size: 0.8rem; font-weight: 700;
  padding: 6px 16px; border-radius: 20px;
  text-transform: uppercase; letter-spacing: 0.05em;
  box-shadow: 0 2px 10px rgba(0, 0, 0, 0.22);
  white-space: nowrap; pointer-events: none;
}
.sfp-lobby-card-visual .bi {
  /* v5.2.2 — item 1.10.e. Bumped 4rem → 5rem per user request
   * ("icon can be a bit larger even"). Mobile rule below stays at
   * 3rem because the smaller card aspect needs the icon to breathe. */
  font-size: 5rem;
}

/* v5.2.6 — item 2b. White icon on dark gradients. Three of the 18
 * curated presets (twilight, ember, charcoal) have an average Rec.
 * 601 luminance below 0.6, so the default light-grey icon
 * (#adb5bd, set on .sfp-lobby-card-visual) lacks contrast. The
 * source of truth for which presets are dark is GRADIENT_PRESETS in
 * lib/domain/card-visual.ts (`is_dark: true`); these selectors
 * mirror that data. When a new dark preset is added, add a row
 * here AND set is_dark in the registry — the test in
 * test-v52600-* enforces parity between the two.
 *
 * Threshold and per-preset luminance values are documented in
 * CHANGES-v5.2.6.md. */
.sfp-lobby-card-visual.sfp-grad-twilight .bi,
.sfp-lobby-card-visual.sfp-grad-ember .bi,
.sfp-lobby-card-visual.sfp-grad-charcoal .bi {
  color: #fff;
}

/* v7.72.26 — small card-visual thumbnail for leader Design/Prepare challenge
 * rows. Reuses .sfp-lobby-card-visual (so the gradient classes + the dark-icon
 * override above apply for free) and just shrinks it to a fixed tile. Declared
 * AFTER .sfp-lobby-card-visual so the equal-specificity size/flex/icon-size
 * props win by source order. */
.sf-cv-thumb {
  width: 38px;
  height: 38px;
  min-height: 0;
  flex: 0 0 auto;
  border-radius: 7px;
  border: 1px solid var(--bs-border-color);
}
.sf-cv-thumb .bi { font-size: 1.15rem; }
/* v12.0.341 (feedback 1a) — the live challenge card's header square: sized to
   the session-eyebrow + title + role-badge stack so the visual rides LEFT of
   the titles instead of a full-width banner under them. */
.sf-cv-square {
  width: 56px;
  height: 56px;
  min-height: 0;
  flex: 0 0 auto;
  border-radius: 9px;
  border: 1px solid var(--bs-border-color);
}
.sf-cv-square .bi { font-size: 1.5rem; }
/* v12.0.341 (feedback 1c) — the collapse-header hints (learning "x of y
   visible", leaderboard "participants see…"): white on the coloured summary,
   slightly smaller than .small, and me-4 in markup clears the ::after
   chevron they used to overlap. */
.sf-collapse-hint { color: #fff; font-size: 0.78rem; }
/* v7.72.40 — item 3: full-width card-visual banner for the redesigned live
 * challenge card. Reuses .sfp-lobby-card-visual (gradients + dark-icon override
 * + the .has-image img rule below all apply for free); just makes it a short,
 * full-bleed banner with a mid-size icon. Declared after .sfp-lobby-card-visual
 * so the equal-specificity size/icon props win by source order. */
.sf-cv-banner {
  width: 100%;
  min-height: 0;
  height: 116px;
  flex: 0 0 auto;
  border-radius: 0;
}
.sf-cv-banner .bi { font-size: 3.25rem; }
@media (max-width: 575.98px) {
  .sf-cv-banner { height: 96px; }
  .sf-cv-banner .bi { font-size: 2.75rem; }
}
/* Image variant — image fills the area, no gradient backdrop. */
.sfp-lobby-card-visual.has-image {
  background: none;
  padding: 0;
}
.sfp-lobby-card-visual.has-image img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
}
.sfp-lobby-card.active-challenge .sfp-lobby-card-visual:not(.has-image) {
  background: linear-gradient(135deg, rgba(13,165,242,0.08) 0%, rgba(13,165,242,0.16) 100%);
  color: var(--bs-primary);
}
/* v5.2.2 — item 2.2. Per-exercise gradient presets.
 * Applied as an extra class on .sfp-lobby-card-visual; the rules
 * below override BOTH the default grey gradient (line 1591) AND
 * the active-challenge blue overlay (immediately above) — when
 * the leader has picked a gradient for an exercise we honour that
 * choice regardless of active state. The active-challenge state
 * still surfaces through the icon colour rule below (icon goes
 * brand-blue when active, gradient stays) plus other lobby
 * affordances (status badge, time chip).
 *
 * Each rule mirrors a GRADIENT_PRESETS entry in card-visual.ts.
 * The .sfp-lobby-card-visual.has-image rule on line 1605 still
 * wins via the `background:none` declaration (the image fully
 * covers the area, no gradient visible).
 *
 * 18 presets — kept synced with the registry. Adding/removing a
 * preset = one entry in card-visual.ts + one rule here. */
.sfp-lobby-card-visual.sfp-grad-slate     { background: linear-gradient(135deg, #f1f3f5 0%, #e9ecef 100%); }
.sfp-lobby-card-visual.sfp-grad-sky       { background: linear-gradient(135deg, #e3f2fd 0%, #bbdefb 100%); }
.sfp-lobby-card-visual.sfp-grad-ocean     { background: linear-gradient(135deg, #b3e5fc 0%, #4fc3f7 100%); }
.sfp-lobby-card-visual.sfp-grad-mint      { background: linear-gradient(135deg, #e0f2f1 0%, #b2dfdb 100%); }
.sfp-lobby-card-visual.sfp-grad-forest    { background: linear-gradient(135deg, #c8e6c9 0%, #81c784 100%); }
.sfp-lobby-card-visual.sfp-grad-sage      { background: linear-gradient(135deg, #dcedc8 0%, #aed581 100%); }
.sfp-lobby-card-visual.sfp-grad-lemon     { background: linear-gradient(135deg, #fff9c4 0%, #fff176 100%); }
.sfp-lobby-card-visual.sfp-grad-sand      { background: linear-gradient(135deg, #fff8e1 0%, #ffe0b2 100%); }
.sfp-lobby-card-visual.sfp-grad-peach     { background: linear-gradient(135deg, #ffe0b2 0%, #ffab91 100%); }
.sfp-lobby-card-visual.sfp-grad-coral     { background: linear-gradient(135deg, #ffccbc 0%, #ff8a65 100%); }
.sfp-lobby-card-visual.sfp-grad-rose      { background: linear-gradient(135deg, #fce4ec 0%, #f8bbd0 100%); }
.sfp-lobby-card-visual.sfp-grad-cherry    { background: linear-gradient(135deg, #ffcdd2 0%, #ef9a9a 100%); }
.sfp-lobby-card-visual.sfp-grad-lavender  { background: linear-gradient(135deg, #e1bee7 0%, #ce93d8 100%); }
.sfp-lobby-card-visual.sfp-grad-plum      { background: linear-gradient(135deg, #c5cae9 0%, #9fa8da 100%); }
.sfp-lobby-card-visual.sfp-grad-aurora    { background: linear-gradient(135deg, #b39ddb 0%, #80cbc4 100%); }
.sfp-lobby-card-visual.sfp-grad-twilight  { background: linear-gradient(135deg, #5c6bc0 0%, #26418f 100%); color: rgba(255,255,255,0.9); }
.sfp-lobby-card-visual.sfp-grad-ember     { background: linear-gradient(135deg, #ffab91 0%, #d84315 100%); color: rgba(255,255,255,0.9); }
.sfp-lobby-card-visual.sfp-grad-charcoal  { background: linear-gradient(135deg, #607d8b 0%, #37474f 100%); color: rgba(255,255,255,0.9); }
/* Same selector that adds the brand-blue tint on active challenges
 * must yield to a gradient preset, so we restate the gradient at a
 * more-specific selector. Without this rule the active-state
 * background-replacement above would clobber the preset. */
.sfp-lobby-card.active-challenge .sfp-lobby-card-visual.sfp-grad-slate     { background: linear-gradient(135deg, #f1f3f5 0%, #e9ecef 100%); }
.sfp-lobby-card.active-challenge .sfp-lobby-card-visual.sfp-grad-sky       { background: linear-gradient(135deg, #e3f2fd 0%, #bbdefb 100%); }
.sfp-lobby-card.active-challenge .sfp-lobby-card-visual.sfp-grad-ocean     { background: linear-gradient(135deg, #b3e5fc 0%, #4fc3f7 100%); }
.sfp-lobby-card.active-challenge .sfp-lobby-card-visual.sfp-grad-mint      { background: linear-gradient(135deg, #e0f2f1 0%, #b2dfdb 100%); }
.sfp-lobby-card.active-challenge .sfp-lobby-card-visual.sfp-grad-forest    { background: linear-gradient(135deg, #c8e6c9 0%, #81c784 100%); }
.sfp-lobby-card.active-challenge .sfp-lobby-card-visual.sfp-grad-sage      { background: linear-gradient(135deg, #dcedc8 0%, #aed581 100%); }
.sfp-lobby-card.active-challenge .sfp-lobby-card-visual.sfp-grad-lemon     { background: linear-gradient(135deg, #fff9c4 0%, #fff176 100%); }
.sfp-lobby-card.active-challenge .sfp-lobby-card-visual.sfp-grad-sand      { background: linear-gradient(135deg, #fff8e1 0%, #ffe0b2 100%); }
.sfp-lobby-card.active-challenge .sfp-lobby-card-visual.sfp-grad-peach     { background: linear-gradient(135deg, #ffe0b2 0%, #ffab91 100%); }
.sfp-lobby-card.active-challenge .sfp-lobby-card-visual.sfp-grad-coral     { background: linear-gradient(135deg, #ffccbc 0%, #ff8a65 100%); }
.sfp-lobby-card.active-challenge .sfp-lobby-card-visual.sfp-grad-rose      { background: linear-gradient(135deg, #fce4ec 0%, #f8bbd0 100%); }
.sfp-lobby-card.active-challenge .sfp-lobby-card-visual.sfp-grad-cherry    { background: linear-gradient(135deg, #ffcdd2 0%, #ef9a9a 100%); }
.sfp-lobby-card.active-challenge .sfp-lobby-card-visual.sfp-grad-lavender  { background: linear-gradient(135deg, #e1bee7 0%, #ce93d8 100%); }
.sfp-lobby-card.active-challenge .sfp-lobby-card-visual.sfp-grad-plum      { background: linear-gradient(135deg, #c5cae9 0%, #9fa8da 100%); }
.sfp-lobby-card.active-challenge .sfp-lobby-card-visual.sfp-grad-aurora    { background: linear-gradient(135deg, #b39ddb 0%, #80cbc4 100%); }
.sfp-lobby-card.active-challenge .sfp-lobby-card-visual.sfp-grad-twilight  { background: linear-gradient(135deg, #5c6bc0 0%, #26418f 100%); }
.sfp-lobby-card.active-challenge .sfp-lobby-card-visual.sfp-grad-ember     { background: linear-gradient(135deg, #ffab91 0%, #d84315 100%); }
.sfp-lobby-card.active-challenge .sfp-lobby-card-visual.sfp-grad-charcoal  { background: linear-gradient(135deg, #607d8b 0%, #37474f 100%); }

/* v5.2.1 — item 9c. Status area moves to BOTTOM-LEFT (was top-right).
 * Sits above the visual via z-index. Floats on the card's bottom
 * edge so it doesn't crowd the title at top. The chevron CTA stays
 * BOTTOM-RIGHT for affordance. */
.sfp-lobby-card-status {
  position: absolute;
  bottom: 0.875rem;
  left: 0.875rem;
  display: flex;
  gap: 0.4rem;
  z-index: 2;
  align-items: center;
  flex-wrap: wrap;
}

/* Chevron-in-circle CTA — BOTTOM-RIGHT (unchanged position). */
.sfp-lobby-card-cta {
  position: absolute;
  bottom: 0.875rem;
  right: 0.875rem;
  width: 36px;
  height: 36px;
  border-radius: 50%;
  background: #1a1d21;
  color: #fff;
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 1.05rem;
  transition: transform 0.15s, background 0.15s;
  z-index: 2;
}
.sfp-lobby-card:hover .sfp-lobby-card-cta {
  transform: translateX(2px);
  background: var(--bs-primary);
}

/* Mobile tuning. */
@media (max-width: 480px) {
  .sfp-lobby-card { min-height: 260px; }
  .sfp-lobby-card-visual:not(.sf-cv-thumb):not(.sf-cv-banner) { min-height: 140px; }
  .sfp-lobby-card-visual:not(.sf-cv-thumb):not(.sf-cv-banner) .bi { font-size: 3rem; }
  .sfp-lobby-card-title { font-size: 1.05rem; }
  .sfp-lobby-card-status { bottom: 0.625rem; left: 0.625rem; }
  .sfp-lobby-card-cta {
    width: 32px; height: 32px;
    bottom: 0.625rem; right: 0.625rem;
    font-size: 0.95rem;
  }
}
/* v5.0.49 — bug 2. Time-remaining chip on lobby cards. Subtle grey
 * style so it's a secondary signal compared to the blue Live pill;
 * the Live pill is the primary "you can act here" cue. Uses inline-
 * flex + small gap to align the bi-clock icon with the numeric text. */
.sfp-lobby-remaining {
  display: inline-flex; align-items: center; gap: 4px;
  font-size: 0.72rem; font-weight: 600;
  color: #5a6270;
  background: #f1f3f5;
  padding: 3px 8px; border-radius: 16px;
  white-space: nowrap;
  font-variant-numeric: tabular-nums;
}
.sfp-lobby-remaining .bi-clock { font-size: 0.85rem; }
/* Urgent state — last 60s. Switches to a warning palette so the
 * participant knows time's almost up. Matches the urgent state of
 * .sfp-countdown on the single-challenge page. */
.sfp-lobby-remaining.urgent {
  color: #842029;
  background: #f8d7da;
}
/* v12.0.77 (AN.1a phase 2) — the lobby chip adopts the shared urgency ring
   (replaces the clock icon) + adaptive band tint, so it escalates by the
   challenge's length like the challenge-page countdown. `.urgent` above is the
   no-helper fallback; calm = the default grey palette. */
.sfp-lobby-ring { width: 18px; height: 18px; display: inline-flex; align-items: center; justify-content: center; flex: 0 0 auto; }
.sfp-lobby-ring .bi-clock { font-size: 0.78rem; }
.sfp-lobby-remaining[data-band="warn"] { color: #8a5a06; background: #fff3cd; }
.sfp-lobby-remaining[data-band="crit"] { color: #842029; background: #f8d7da; }
/* v12.0.78 (AN.1a phase 2b) — ring host inside the leader timer button (white
   arc on the solid colour-coded button; the button colour carries the band). */
.sf-timer-ring { width: 16px; height: 16px; display: inline-flex; align-items: center; justify-content: center; flex: 0 0 auto; }
/* v12.0.78 (AN.1a phase 2b) — ring host inside the leader timer button (white
   arc on the solid colour-coded button; the button colour carries the band). */
.sf-timer-ring { width: 16px; height: 16px; display: inline-flex; align-items: center; justify-content: center; flex: 0 0 auto; }
.sfp-lobby-card-body {
  padding: 1rem 1.25rem;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1rem;
}
.sfp-lobby-card-name {
  font-weight: 600;
  font-size: 0.95rem;
  color: #1a1d21;
}
/* v3.44.39 — #6: was #34c759 (green). Switched to blue so the participant
 * lobby's "Live" pill matches the leader-side live-mode button (also blue)
 * and the JS-rendered lobby badge (text-bg-primary). #0d6efd is Bootstrap
 * primary; close enough to the leader-side colour scheme to read as the
 * same UX state.
 *
 * v5.0.49 — bug 5. Pre-v5.0.49 a CSS-only pulsing dot was rendered via
 * ::before. User wanted full visual consistency with the admin-side
 * Live tab (which uses bi-broadcast + "Live"). v5.0.49: drop the
 * ::before pulsing dot, leaving the markup to supply a bi-broadcast
 * icon inline. The pill keeps its blue color + bold uppercase Live
 * text. Same visual on admin Live tab + participant single-challenge
 * top-right + participant home active blocks. */
.sfp-status-open {
  display: inline-flex; align-items: center; gap: 5px;
  /* v5.0.50 — bug 4. Pre-v5.0.50 used #0d6efd (Bootstrap default
   * primary). User said it reads too dark vs. the brand blue used
   * elsewhere. The site overrides --bs-primary to #0DA5F2 (lighter,
   * cyan-leaning) at :root; bind the pill to that variable so the
   * Live indicator matches the rest of the brand palette without
   * a parallel hardcoded hex floating around. */
  background: var(--bs-primary); color: #fff;
  font-size: 0.72rem; font-weight: 700;
  padding: 3px 10px; border-radius: 20px;
  white-space: nowrap;
  text-transform: uppercase; letter-spacing: 0.04em;
}
.sfp-status-open .bi-broadcast {
  font-size: 0.85rem;
  /* v5.0.49 — gentle pulse on the broadcast icon — keeps the "this
   * is live, right now" cue from the old ::before dot without
   * adding a separate dot element. */
  animation: sfp-pulse-broadcast 1.5s ease-in-out infinite;
}
@keyframes sfp-pulse-broadcast {
  0%, 100% { opacity: 0.7; }
  50%       { opacity: 1;   }
}

/* ── Register page ── */
.sfp-register-card {
  background: #fff;
  border-radius: var(--sfp-radius);
  box-shadow: var(--sfp-shadow-md);
  padding: 2.5rem 2rem;
  max-width: 420px;
  width: 100%;
}
@media (max-width: 480px) {
  .sfp-register-card { padding: 2rem 1.25rem; border-radius: 0; box-shadow: none; }
}
/* ── Panel 4 fixes ── */

/* 1. is-answered overrides focus-within glow — green takes priority */
.sfp-question.is-answered:focus-within {
  border-color: var(--sfp-answered);
  box-shadow: 0 0 0 3px var(--sfp-answered-light), var(--sfp-shadow);
}

/* 2. Content card: add subtle border so it reads as a card on grey bg */
.sfp-content {
  border: 1px solid #e8eaed;
}

/* 3. Countdown pulse when urgent */
.sfp-countdown.urgent {
  animation: sfp-countdown-pulse 1.5s ease-in-out infinite;
}
@keyframes sfp-countdown-pulse {
  0%, 100% { box-shadow: none; }
  50% { box-shadow: 0 0 0 3px rgba(230, 126, 34, 0.25); }
}

/* 4. Floating progress pill */
.sfp-floating-progress {
  position: fixed;
  bottom: 1.5rem;
  left: 50%;
  transform: translateX(-50%);
  background: rgba(26, 29, 33, 0.88);
  color: #fff;
  font-size: 0.78rem;
  font-weight: 600;
  padding: 0.375rem 1rem;
  border-radius: 20px;
  backdrop-filter: blur(8px);
  -webkit-backdrop-filter: blur(8px);
  box-shadow: 0 4px 12px rgba(0,0,0,0.2);
  z-index: 100;
  white-space: nowrap;
  pointer-events: none;
  opacity: 0;
  transition: opacity 0.3s;
}
.sfp-floating-progress.visible { opacity: 1; }
.sfp-floating-progress .sfp-fp-done {
  color: #4ade80;
}

/* ─── v3.44.11 — design-mode edit-mode toggle ──────────────────────────
   Design mode opens in read-only view by default. Clicking the Edit
   button at the top adds .is-editing to #mode-design, which reveals the
   editing affordances (drag handles, delete buttons, rename pencils,
   add controls). The selectors below match the structures that already
   exist in _journey-design-mode.ejs — no markup changes were needed.

   Notes:
   - Live (active) and closed challenges remain non-draggable regardless
     of edit mode (handled by Sortable's filter + the row's own opacity).
   - The challenge "Add" button at the bottom of each session uses
     [data-bs-target="#add-challenge-modal"] — easy to target.
   - The new-session card at the bottom is the only `.border-dashed`
     card inside #mode-design, so the selector is unambiguous.
*/

#mode-design:not(.is-editing) .session-drag-handle,
#mode-design:not(.is-editing) .challenge-drag-handle,
#mode-design:not(.is-editing) .challenge-rename-btn,
#mode-design:not(.is-editing) [data-bs-target^="#design-rename-"],
#mode-design:not(.is-editing) form[action$="/delete"],
#mode-design:not(.is-editing) form[action$="/remove"],
#mode-design:not(.is-editing) [data-bs-target="#add-challenge-modal"],
#mode-design:not(.is-editing) .sf-edit-only,
#mode-design:not(.is-editing) .card.border-dashed {
  display: none !important;
}

/* Add-challenge button container that becomes empty in read-only mode
   (only contained the button + its top-border padding). Hide the border
   so we don't get a thin stripe with no content. */
#mode-design:not(.is-editing) .px-3.py-2.border-top:has([data-bs-target="#add-challenge-modal"]) {
  display: none !important;
}

/* v8.3.2 — #8.2a: the yellow dashed outline around each session in edit mode
   was removed. The revealed drag handles, delete buttons, and rename pencils
   already signal that the area is interactive; the dashed boxes were noise. */
/* v7.72.21 — Edit toggle is now btn-primary (was btn-warning). This rule is
 * effectively dead (cockpit.js hides #design-edit-toggle with d-none while
 * editing), but kept consistent with the blue button should it ever show. */
#mode-design.is-editing #design-edit-toggle {
  background-color: var(--bs-primary);
  border-color: var(--bs-primary);
  color: #fff;
}

/* ─── Design-edit pending-state styles (v3.44.31) ────────────────────────
 * When the user renames or deletes an item in design-edit mode and the
 * action is buffered (not yet sent to the server), the affected row gets
 * one of these classes so the user can see what they've staged before
 * clicking Save.
 *
 * design-pending-rename: a subtle visual cue on the name itself (the
 * user already sees the new name in place; the asterisk + colour signal
 * "this is your local edit, not yet saved"). Hover tooltip explains.
 *
 * design-pending-delete: a stronger visual — strikethrough + dimmed
 * opacity — so the user can SEE which items are about to vanish on
 * Save. The Undo button (injected by cockpit.js) lets them roll back
 * a single pending delete without discarding their other pending
 * changes.
 *
 * Cancel reloads the page, so all pending visuals reset to server state.
 */
.design-pending-rename {
  font-style: italic;
  color: var(--bs-warning-text-emphasis, #997404);
}
.design-pending-rename::after {
  content: " *";
  font-weight: bold;
  color: var(--bs-warning, #ffc107);
}

.design-pending-delete {
  /* v3.44.39 — #10: light-red background so pending-delete is clearly visible
   * when scanning a long session list. v8.3.2 — #8.2c: the row-wide
   * strikethrough was removed — it bled onto the injected Undo button (whose
   * text must stay legible) and the background + dimming already carry the
   * "about to vanish on Save" signal. */
  background-color: rgba(220, 53, 69, 0.08);
  border-color: rgba(220, 53, 69, 0.35) !important;
  opacity: 0.85;
}
/* v3.44.39 — #10: hide the delete form/icon while in pending-delete state
 * (the Undo button replaces it). The selectors mirror the design-mode
 * deletion forms that cockpit.js intercepts (sessions: /delete,
 * challenges: /remove). */
.design-pending-delete form[action$="/delete"],
.design-pending-delete form[action$="/remove"] {
  display: none;
}
.design-pending-delete .design-pending-delete {
  /* nested: don't double-apply (a challenge inside a deleted session
     would otherwise get both, looking weird) */
  text-decoration: none;
  opacity: 1;
}
/* Restore full opacity on the Undo button itself so it's clearly
 * interactive even within a strikethrough row. */
.design-pending-delete [data-undo-pending-delete] {
  opacity: 1;
  text-decoration: none;
}

/* v8.3.2 — #8.2d: when an entire session is staged for deletion, its child
   challenge rows also show the light-red wash so the leader sees everything
   that vanishes on Save. They get NO Undo of their own (the single Undo sits
   next to the session's delete control) and their delete forms are already
   hidden by the .design-pending-delete form[action$="/remove"] rule above. */
.session-block.design-pending-delete .challenge-design-row {
  background-color: rgba(220, 53, 69, 0.08);
  border-color: rgba(220, 53, 69, 0.30) !important;
}

/* design-pending-create (v3.44.32): styled card/row for a session or
 * challenge that's been queued for creation but not yet sent to the
 * server. Distinguishable from real items via a subtle blue tint
 * (matches the "new" badge inside) and a slight opacity to signal
 * "this isn't real yet." Cancel discards via reload, Save creates and
 * reloads.
 */
.design-pending-create {
  /* v8.3.2 — #8.2b: added (not-yet-saved) items use a light-yellow wash
     instead of a blue dashed border — softer, and consistent with the
     "staged change" warning palette used elsewhere. */
  background-color: rgba(255, 193, 7, 0.12);  /* light-yellow / warning subtle */
  border-color: var(--bs-warning-border-subtle, #ffe69c);
  opacity: 1;
}
.session-block.design-pending-create {
  border-width: 2px;
}

/* v3.56.0 — item 1: Time's up state hides challenge content. When the
 * tick detects remaining <= 0 it adds .time-expired to <body>, which
 * hides all .sfp-content and .sfp-question blocks so the participant
 * sees only the injected #sfp-time-up-banner. Extension by the leader
 * removes the class and the elements come back.
 * v7.72.10 — the interact answer now renders in .sfp-answer-card (inside
 * the chat card) rather than .sfp-question, so hide that too. The chat
 * bar + discussion stay visible on expiry (matching the pre-v7.72.10
 * behaviour where .sfp-chat was a separate, un-hidden sibling). */
body.time-expired .sfp-content,
body.time-expired .sfp-question,
body.time-expired .sfp-answer-card {
  display: none !important;
}
/* v12.0.228 (user round-3 item 3d) — hide the WHOLE elements section on
   expiry, not just content/questions/answer-cards: interact chat panels
   ("Tap to respond"), charts, images and labels all stayed visible after
   "Time's up". The banner is inserted BEFORE #sfp-elements-section (both the
   SSR and JS paths), so only the banner remains; a leader time-extension
   removes body.time-expired and everything returns. The older three rules
   above stay as a safety net for any element rendered outside the section. */
body.time-expired #sfp-elements-section {
  display: none !important;
}
/* v14.41.0 (user report 3c) — "should only display when questions are
   displayed". The progress label + bar are rendered OUTSIDE
   #sfp-elements-section (they precede it in _participant-main.ejs), so the
   rule above hid every question on expiry while "0 of 1 question answered"
   stayed on screen above the "Time's up" banner, counting questions the
   participant could no longer see. They describe the elements section, so
   they hide and return with it — the leader extending the timer removes
   body.time-expired and both come back together.
   #sfp-fp (the floating progress pill) is dead code today — nothing renders
   it, as challenge.js's swapMainSection note records — but it is the same
   kind of surface, so it is covered here rather than being a fresh bug the
   day someone re-introduces it. */
body.time-expired .sfp-progress-label,
body.time-expired .sfp-progress,
body.time-expired #sfp-fp {
  display: none !important;
}
.sfp-time-up {
  background: #fff;
  border-radius: var(--sfp-radius);
  box-shadow: var(--sfp-shadow);
  padding: 2rem 1.25rem;
  margin: 0 0 1rem;
}
@media (min-width: 576px) {
  .sfp-time-up { padding: 2.5rem 1.5rem; }
}

/* v3.60.0 — hover/focus affordance on the group-members badge button.
 * The badge is a Bootstrap modal trigger styled as a pill; without an
 * obvious hover state the participant may not realise it's tappable
 * even with the bi-info-circle hint inside. Slight darken on hover +
 * focus ring to make the click affordance unambiguous. */
button.badge[data-bs-toggle="modal"] {
  transition: background-color 0.15s, transform 0.05s;
}
button.badge[data-bs-toggle="modal"]:hover {
  background-color: rgba(13, 165, 242, 0.22) !important;
}
button.badge[data-bs-toggle="modal"]:active {
  transform: scale(0.97);
}
button.badge[data-bs-toggle="modal"]:focus-visible {
  outline: 2px solid var(--sfp-brand, #0DA5F2);
  outline-offset: 2px;
}


/* v4.5.1 — project chevron toggle on /l/dashboard
   Points right when collapsed (default), rotates to point down when
   expanded. Bootstrap adds the `.collapsed` class to the trigger when
   the target is hidden — we read that class to drive the rotation.
   Coloured primary so the affordance is visible. */
.sf-project-chevron {
  color: var(--bs-primary);
}
.sf-project-chevron .bi {
  transition: transform 150ms ease-in-out;
  display: inline-block;
}
.sf-project-chevron:not(.collapsed) .bi {
  transform: rotate(90deg);
}

/* v4.5.1 — /l/live card hover state. Whole card is a link; lift on
   hover so the click affordance is clear. The "Open cockpit" button
   inside is pe-none (purely visual) so the parent link captures the
   click without nested-anchor weirdness.

   v5.2.2 — item 1.8. Pre-v5.2.2 the hover was a 1px translate plus a
   soft blue-tinted box-shadow — user reported it as "too subtle, I
   need a clearer indication that we can click." Swapped to a full
   blue fill on hover, with text + nested-button colours inverted to
   white so contrast stays AAA on the new background. The lift is
   kept (cheap motion cue) but the shadow is dropped — fill is
   carrying that signal now. Transition extended to background-color
   so the fill animates in instead of snapping. */
.sf-live-card {
  transition:
    transform 120ms ease,
    background-color 120ms ease,
    color 120ms ease;
}

/* v5.3.1 — Dashboard polish.
 *
 * .sf-dashboard-project   — the project card; subtle drop-shadow that
 *                            lifts on hover (signals "this contains
 *                            content you can interact with" without
 *                            stealing the blue fill that .sf-live-card
 *                            uses for clickable cards — the project
 *                            card itself isn't a click target, only
 *                            the inner Edit-journeys button and the
 *                            journey rows are).
 * .sf-dashboard-journey   — each journey row inside a project. On
 *                            hover the whole row paints brand-blue
 *                            with white text. The inner anchor +
 *                            meta span inherit; the paused/live
 *                            badges keep their fills (their own
 *                            colour vocabulary stays meaningful).
 * .sf-dashboard-empty     — the no-projects-yet card; light bg,
 *                            no hover. Distinct from .sf-live-card
 *                            so a sparse account doesn't get a
 *                            misleading "click me" affordance on
 *                            an empty state. */

.sf-dashboard-project {
  border-color: var(--bs-border-color);
  box-shadow: 0 1px 3px rgba(0, 0, 0, 0.04);
  transition: box-shadow 150ms ease, transform 150ms ease;
}
.sf-dashboard-project:hover {
  box-shadow: 0 4px 10px rgba(0, 0, 0, 0.06);
  transform: translateY(-1px);
}

.sf-dashboard-journey {
  transition:
    background-color 120ms ease,
    border-color 120ms ease,
    color 120ms ease;
  cursor: pointer;
}
.sf-dashboard-journey:hover,
.sf-dashboard-journey:focus-within {
  background-color: var(--bs-primary) !important;
  border-color: var(--bs-primary) !important;
  color: #fff;
}
/* Paint the inner anchor + name + meta span white on hover so contrast
 * stays AAA on the brand-blue background. The link reset (.text-decoration-none)
 * stays in place; only colour changes. */
.sf-dashboard-journey:hover .sf-dashboard-journey-link,
.sf-dashboard-journey:focus-within .sf-dashboard-journey-link,
.sf-dashboard-journey:hover .sf-dashboard-journey-name,
.sf-dashboard-journey:focus-within .sf-dashboard-journey-name,
.sf-dashboard-journey:hover .sf-dashboard-journey-meta,
.sf-dashboard-journey:focus-within .sf-dashboard-journey-meta {
  color: #fff !important;
}
/* Inner badges (paused/live pills) on hover. The Paused badge has its
 * own warning yellow fill that reads fine on blue, but the icon could
 * use a touch more pop — leave the badges alone, they're already
 * legible by virtue of being filled badges with their own colour. */

.sf-dashboard-empty {
  border-color: var(--bs-border-color);
}

.sf-live-card:hover,
.sf-live-card:focus-visible {
  transform: translateY(-1px);
  background-color: var(--bs-primary) !important;
  color: #fff !important;
}
/* Invert anything inside the hovered card that carried colour from
 * Bootstrap utilities (.text-body-secondary, .text-primary, etc.).
 * The :where() wrapper keeps specificity low so author overrides on
 * individual cards still win. */
.sf-live-card:hover :where(
  .text-body-secondary, .text-body-tertiary, .text-muted,
  .text-primary, .text-dark, h1, h2, h3, h4, h5, h6, p, span
),
.sf-live-card:focus-visible :where(
  .text-body-secondary, .text-body-tertiary, .text-muted,
  .text-primary, .text-dark, h1, h2, h3, h4, h5, h6, p, span
) {
  color: #fff !important;
}
/* Nested outline-primary buttons inside the card invert too — the
 * card is the click target so the button is decorative; matching it
 * to the parent's hovered colour avoids the "two affordances at
 * different weights" look. */
.sf-live-card:hover .btn.btn-outline-primary,
.sf-live-card:hover .btn.btn-primary,
.sf-live-card:focus-visible .btn.btn-outline-primary,
.sf-live-card:focus-visible .btn.btn-primary {
  /* v5.2.3 → v5.2.4 fixup. The text-inversion rule above carries
   * !important and matches <span class="btn btn-primary"> (the
   * "Open cockpit" button is a span, not a button — see live.ejs
   * line 80). Without !important on these declarations, the
   * inversion's `color: #fff !important` wins despite this rule
   * having higher specificity, and the button reads as a blank
   * white rectangle on hover. With !important the override wins,
   * so the button shows white background + brand-blue text +
   * brand-blue icon on hover. */
  background-color: #fff !important;
  color: var(--bs-primary) !important;
  border-color: #fff !important;
}
/* The icon inside .btn.btn-primary inherits color via the cascade.
 * .sf-live-card:hover .text-body-secondary,...,span sets the span's
 * color, and the <i> bootstrap-icon inside the span inherits that.
 * Explicit !important reassertion on icons inside primary buttons
 * keeps them brand-blue on hover. */
.sf-live-card:hover .btn.btn-primary .bi,
.sf-live-card:hover .btn.btn-outline-primary .bi,
.sf-live-card:focus-visible .btn.btn-primary .bi,
.sf-live-card:focus-visible .btn.btn-outline-primary .bi {
  color: var(--bs-primary) !important;
}
/* v5.3.1 — Keep .badge.text-bg-light readable on .sf-live-card hover.
 * The catch-all hover rule above sets `span` colour to white, which
 * also hits tag-style badges (the outlines page uses
 * `<span class="badge text-bg-light border">…</span>` for tag chips).
 * On hover the badge's light-grey background stayed but the text went
 * white → unreadable. This rule restores dark text on tag badges
 * inside hovered cards, scoped to the .text-bg-light variant so other
 * badge variants (text-bg-primary, text-bg-warning, etc.) keep their
 * own colour vocabulary. */
.sf-live-card:hover .badge.text-bg-light,
.sf-live-card:focus-visible .badge.text-bg-light,
.sf-live-card:hover .badge.text-bg-light *,
.sf-live-card:focus-visible .badge.text-bg-light * {
  color: #212529 !important;
}

/* v5.2.2 — fixup for bug 1 (the first item in user's next turn).
 * The card-wide colour inversion on hover (above) made everything
 * inside the card white via inheritance — fine for the blue area,
 * but the inner "Running: …" pill has its own .bg-body-tertiary
 * light background, so white text on light pill became unreadable.
 * Restore body colour for that pill and everything inside it.
 * Specificity is one notch above the inversion rule so this wins
 * even with the !important on the other side. */
.sf-live-card:hover .bg-body-tertiary,
.sf-live-card:focus-visible .bg-body-tertiary {
  color: var(--bs-body-color, #212529);
}
.sf-live-card:hover .bg-body-tertiary :where(
  .text-body-secondary, .text-body-tertiary, .text-muted,
  .text-primary, .text-dark, h1, h2, h3, h4, h5, h6, p, span, strong, div, i
),
.sf-live-card:focus-visible .bg-body-tertiary :where(
  .text-body-secondary, .text-body-tertiary, .text-muted,
  .text-primary, .text-dark, h1, h2, h3, h4, h5, h6, p, span, strong, div, i
) {
  color: var(--bs-body-color, #212529) !important;
}

/* v5.2.2 — item 1.9. Outline library cards reuse .sf-live-card hover
 * + transition rules. The outline card is rendered as a <button>
 * element (whole card is the click target → opens the view modal);
 * the rules below normalise away the native button chrome so the
 * card reads as a card, not a control. .sf-live-card text colour
 * inversion already covers the hover; we just need the resting
 * state to look right. */
button.sf-outline-card {
  background-color: var(--bs-body-bg);
  appearance: none;
  -webkit-appearance: none;
  cursor: pointer;
}
button.sf-outline-card:focus {
  outline: none;
}
button.sf-outline-card:focus-visible {
  outline: 2px solid var(--bs-primary);
  outline-offset: 2px;
}

/* ─────────────────────────────────────────────────────────────────
 * Timer modal — adjust grid blocks (v5.2.4)
 * Three DAY/HOUR/MIN visual blocks. Each is a card-like container
 * binding its heading to its four +/− buttons. Per-block grey tint
 * (DAY=black, HOUR=dark grey, MIN=medium grey) replaces the green/red
 * coding from v5.2.3 — the column identity is the dominant signal.
 * Buttons inside each block still distinguish + from − via fill vs
 * outline; the tint family stays the same within a block.
 * ─────────────────────────────────────────────────────────────── */
.sfp-timer-block {
  border: 1px solid var(--bs-border-color);
  border-radius: 0.5rem;
  padding: 0.5rem;
  background-color: var(--bs-body-bg);
}
.sfp-timer-block-heading {
  text-align: center;
  font-size: 0.75rem;
  font-weight: 600;
  letter-spacing: 0.05em;
  margin-bottom: 0.5rem;
  text-transform: uppercase;
}

/* v5.3.0 — Timer adjust buttons unified.
 *
 * Pre-v5.3.0 the day / hour / min ± buttons used per-unit colour pairs
 * (black-solid/black-outline for DAY, dark-grey-solid/dark-grey-outline
 * for HOUR, medium-grey-solid/medium-grey-outline for MIN). The
 * per-unit gradation tried to reinforce "DAY is bigger than HOUR is
 * bigger than MIN" via visual hierarchy. In practice the column
 * labels (DAY / HOUR / MIN above each block) already communicate
 * that; the per-unit shading just made the modal busier without
 * adding signal, and the lighter MIN buttons were borderline
 * low-contrast on white.
 *
 * v5.3.0 collapses to two colours, applied uniformly across all
 * three unit columns:
 *   • + buttons (solid):   dark grey (#495057) on white — for the
 *     three .sfp-timer-btn-{day,hour,min} classes
 *   • − buttons (outline): white on dark-grey border (#495057) —
 *     for the three .sfp-timer-btn-{day,hour,min}-outline classes
 *
 * Per-unit identity is preserved via the column HEADING colours
 * above each block (DAY → black, HOUR → dark grey, MIN → medium
 * grey); those rules sit just below this block.
 *
 * The class names are kept intact (cockpit.js still emits
 * `c.fill = "sfp-timer-btn-{unit}"` and `c.outline =
 * "sfp-timer-btn-{unit}-outline"`); just the underlying styling is
 * unified. A future cleanup could fold the six classes into two,
 * but that would require touching cockpit.js + tests pinned to
 * the per-unit class names. */

/* All three +/− unit columns share the same button styling. */
.sfp-timer-btn-day,
.sfp-timer-btn-hour,
.sfp-timer-btn-min,
.sfp-timer-btn-day:focus,
.sfp-timer-btn-hour:focus,
.sfp-timer-btn-min:focus {
  background-color: #495057;
  color: #fff;
  border: 1px solid #495057;
}
.sfp-timer-btn-day:hover:not(:disabled),
.sfp-timer-btn-hour:hover:not(:disabled),
.sfp-timer-btn-min:hover:not(:disabled) {
  background-color: #343a40;
  color: #fff;
  border-color: #343a40;
}
.sfp-timer-btn-day-outline,
.sfp-timer-btn-hour-outline,
.sfp-timer-btn-min-outline,
.sfp-timer-btn-day-outline:focus,
.sfp-timer-btn-hour-outline:focus,
.sfp-timer-btn-min-outline:focus {
  background-color: #fff;
  color: #495057;
  border: 1px solid #495057;
}
.sfp-timer-btn-day-outline:hover:not(:disabled),
.sfp-timer-btn-hour-outline:hover:not(:disabled),
.sfp-timer-btn-min-outline:hover:not(:disabled) {
  background-color: #495057;
  color: #fff;
  border-color: #495057;
}
.sfp-timer-btn-day:disabled,
.sfp-timer-btn-hour:disabled,
.sfp-timer-btn-min:disabled,
.sfp-timer-btn-day-outline:disabled,
.sfp-timer-btn-hour-outline:disabled,
.sfp-timer-btn-min-outline:disabled {
  opacity: 0.4;
}

/* Per-unit HEADING colours — preserved for column identity in the
 * label row above each ± block. */
.sfp-timer-block:has(.sfp-timer-btn-day) .sfp-timer-block-heading,
.sfp-timer-block-day .sfp-timer-block-heading { color: #212529; }
.sfp-timer-block:has(.sfp-timer-btn-hour) .sfp-timer-block-heading,
.sfp-timer-block-hour .sfp-timer-block-heading { color: #495057; }
.sfp-timer-block:has(.sfp-timer-btn-min) .sfp-timer-block-heading,
.sfp-timer-block-min .sfp-timer-block-heading { color: #52595f; } /* v5.2.6 item 4 — was #6c757d (too light next to HOUR/DAY) */

/* ─────────────────────────────────────────────────────────────────
 * Timer modal — D/H/M summary table (v5.2.4, item 2)
 * Replaces the four stacked "Total time / Time used / Time
 * remaining / New time remaining" rows with a four-column table:
 *   Label | Days | Hours | Minutes
 * Same vertical alignment per unit makes "you have N more days vs
 * now" comparisons readable across rows. Seconds dropped — they're
 * noise at the timer-budget scale.
 * The "New time remaining" row is highlighted (the colour bar the
 * user asked for); the other three rows are dim grey so the
 * highlighted row pops.
 * ─────────────────────────────────────────────────────────────── */
.sfp-timer-summary-table {
  width: 100%;
  margin-bottom: 1rem;
  border-collapse: separate;
  border-spacing: 0;
}
.sfp-timer-summary-table th,
.sfp-timer-summary-table td {
  padding: 0.375rem 0.5rem;
  font-size: 0.875rem;
}
.sfp-timer-summary-table th {
  text-align: right;
  font-size: 0.7rem;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.05em;
  color: var(--bs-secondary-color, #6c757d);
  border-bottom: 1px solid var(--bs-border-color);
}
.sfp-timer-summary-table th:first-child { text-align: left; }
.sfp-timer-summary-table td.sfp-timer-cell-num {
  text-align: right;
  font-family: var(--bs-font-monospace, ui-monospace, monospace);
  font-variant-numeric: tabular-nums;
}
.sfp-timer-summary-row-muted td {
  color: var(--bs-secondary-color, #6c757d);
  background-color: var(--bs-tertiary-bg, #f8f9fa);
}
/* v5.2.6 — item 3. Was: blue-fill row with white text. The "Time
 * remaining" row was removed in v5.2.6 (it was always identical to
 * "New time remaining" at modal-open — purely redundant), and the
 * remaining "New time remaining" row got a quieter treatment: bold
 * brand-blue text on the default background, plus a 2px brand-blue
 * top border to separate it from the Total/Used rows above. Reads
 * as the answer rather than a marketing banner.
 *
 * The corner-radius rules from the previous fill-based design were
 * dropped — without a coloured fill they had nothing to round. */
.sfp-timer-summary-row-highlight td {
  background-color: transparent;
  color: var(--bs-primary);
  font-weight: 700;
  border-top: 2px solid var(--bs-primary);
}

/* v5.2.6 — item 5. Click-feedback pulse on the "New time remaining"
 * cells. When refreshDisplay() detects a cell's text changed, it
 * removes then re-adds .sfp-timer-cell-pulse on that <td>; the
 * keyframes flash a brand-blue tinted background for 350ms so the
 * cause-and-effect link from button click to value change is
 * unmistakable.
 *
 * Why a tinted-bg flash rather than a font-size bump or shake: the
 * D/H/M columns are tabular (.font-variant-numeric: tabular-nums),
 * so a layout-affecting animation would jitter neighbouring cells.
 * A background-only flash is contained to the cell box.
 *
 * The 'forwards' fill-mode + JS removing the class on
 * animationend means a rapid sequence of clicks restarts the
 * animation each time rather than stacking it. */
@keyframes sfp-timer-cell-pulse-kf {
  0%   { background-color: rgba(13, 110, 253, 0.25); }
  100% { background-color: transparent; }
}
.sfp-timer-summary-row-highlight td.sfp-timer-cell-pulse {
  animation: sfp-timer-cell-pulse-kf 350ms ease-out;
  border-radius: 0.25rem;
}

/* v5.2.6 — item 5 (companion). Stronger tactile feedback on the +/−
 * adjust buttons. The default :active state from Bootstrap is a
 * small color shift; on a small button with the per-block grey
 * tint, that shift was barely visible. Add a 50ms scale-down +
 * box-shadow so the click registers physically. The transition
 * matches the modal's existing 0.15s feel. */
.sf-timer-adjust-btn:active:not(:disabled) {
  transform: scale(0.93);
  box-shadow: inset 0 2px 4px rgba(0,0,0,0.15);
}
.sf-timer-adjust-btn {
  transition: transform 80ms ease-out, box-shadow 80ms ease-out, background-color 120ms;
}

/* ─── v6.3 — Participant chat panel ────────────────────────────────────────
 * Per-element collapsible chat under each interact element. WhatsApp-shape
 * bubbles: own = right + green, others = left + white. Sender name shown
 * above "other" bubbles only. The panel is collapsed by default with a
 * small toggle showing 💬 Discussion (with unread badge if any). On expand
 * the messages stream + composer come into view.
 *
 * Sized to slot beneath the existing .sfp-question card without dominating
 * the page. Bubbles wrap; max-width 75% prevents long lines from running
 * edge-to-edge on tablets/desktops.
 */
.sfp-chat {
  margin: 0 0 1rem;
  border-radius: 10px;
  background: #fff;
  border: 1px solid #e4e4e4;
  /* v14.49.0 (participant feedback b+c) — `hidden` made this box the sticky
     composer's containing scrollport: it never scrolls, so the v14.40.0
     "pin the input to the viewport floor" sticky was INERT — the composer's
     screen position was pure flow + whatever the last scroll left, which is
     why its distance to the screen edge differed before vs after Athena's
     reply and could end up glued under the poll pill. `clip` clips the
     rounded corners identically but is NOT a scroll container, so the
     composer finally pins against the real viewport. (The 1px border keeps
     child-margin behaviour unchanged; no JS scrolls this box — measured in
     test-int-chat-composer-clearance.ts.) */
  overflow: clip;
  /* The ONE bottom-clearance for everything pinned at the thread's foot:
     composer + the waiting/locked slots that replace it. 26px clears the
     fixed "Refreshing in Ns" pill (bottom 6px + ~18px tall) and answers the
     "10–20px too low" report; the safe-area term keeps it off the iOS home
     indicator. */
  --sfp-composer-gap: calc(26px + env(safe-area-inset-bottom, 0px));
}
.sfp-chat-toggle {
  display: flex; align-items: center; gap: 8px;
  width: 100%;
  padding: 8px 12px;
  background: transparent;
  border: 0;
  font-size: 0.9rem;
  color: #495057;
  cursor: pointer;
  text-align: left;
  transition: background-color 0.15s ease;
}
.sfp-chat-toggle:focus-visible {
  background: rgba(0,0,0,0.04);
  outline: none;
}
/* v12.0.54 — only apply :hover where a real pointer exists. On touch, :hover
   sticks after a tap, which left a just-tapped collapsed card showing the darker
   indigo-dark variant (looked like inconsistent header colours). */
@media (hover: hover) {
  .sfp-chat-toggle:hover { background: rgba(0,0,0,0.04); }
}
.sfp-chat-toggle-icon { font-size: 1.05rem; color: var(--sfp-indigo); line-height: 1; }
/* v12.0.45 (#9) — when expanded, the answered ✓ in the header reads green
   (collapsed it stays white on the indigo bar, per the rule further down). */
.sfp-interact.is-expanded .sfp-chat-state-icon { color: var(--sfp-answered); }
.sfp-chat-toggle-label { flex: 1; font-weight: 600; }
.sfp-chat-toggle-badge {
  display: inline-block;
  min-width: 18px;
  height: 18px;
  padding: 0 6px;
  border-radius: 9px;
  background: #dc3545;
  color: #fff;
  font-size: 0.7rem;
  font-weight: 600;
  line-height: 18px;
  text-align: center;
}
.sfp-chat-toggle-badge[hidden] { display: none; }
.sfp-chat-toggle-chevron {
  /* item 7 — larger + heavier so the collapse affordance stands out. */
  font-size: 1.05rem;
  font-weight: 700;
  color: #6c757d;
  transition: transform 0.15s ease;
}
/* white on the indigo collapsed bar so it reads clearly */
.sfp-interact:not(.is-expanded) .sfp-chat-toggle-chevron { color: #fff; }
.sfp-chat.is-expanded .sfp-chat-toggle-chevron {
  transform: rotate(180deg);
  color: var(--sfp-indigo-dark);
}
/* v8.3.4 (item B) — interactive elements stand out when collapsed: a coloured
   header bar with a white rocket + white text, so participants see at a glance
   which cards are theirs to act on. Expanding reverts to the neutral header so
   the opened body (answer + conversation) reads cleanly.
   v12.0.45 (#9) — the collapsed bar is now $indigo-300. */
.sfp-interact:not(.is-expanded) .sfp-chat-toggle {
  background: var(--sfp-indigo);
  color: #fff;
}
.sfp-interact:not(.is-expanded) .sfp-chat-toggle:focus-visible {
  background: var(--sfp-indigo-dark);
}
@media (hover: hover) {
  .sfp-interact:not(.is-expanded) .sfp-chat-toggle:hover {
    background: var(--sfp-indigo-dark);
  }
}
/* v12.0.54 (#2) — the OPEN interactive element gets an indigo outline so it's
   clearly the one in focus. border-color stays 1px (no layout shift); the
   box-shadow adds the extra ring. */
.sfp-interact.is-expanded {
  /* v12.0.227 (user item 3h) — light indigo: the heavy full-strength outline
     competed with the content; a soft ring still marks the open panel. */
  border-color: rgba(133, 64, 245, 0.35);
  box-shadow: 0 0 0 1px rgba(133, 64, 245, 0.25);
}
.sfp-interact:not(.is-expanded) .sfp-chat-toggle-icon,
.sfp-interact:not(.is-expanded) .sfp-chat-question,
.sfp-interact:not(.is-expanded) .sfp-chat-state-text,
.sfp-interact:not(.is-expanded) .sfp-chat-state-prompt,
.sfp-interact:not(.is-expanded) .sfp-chat-state-icon,
.sfp-interact:not(.is-expanded) .sfp-chat-toggle-chevron {
  color: #fff;
}
/* v12.0.73 (item 3) — the collapsed header reflects STATE. Once the
   participant/group has answered (`is-answered`) or the turn is with the leader
   (`is-awaiting-leader`), the bar switches from solid indigo (white text =
   "needs my action") to a light-indigo wash with dark text (= "done / waiting
   on the leader"), so the two states read differently at a glance. Unanswered
   keeps the solid indigo above. Higher specificity (extra state class) wins
   over the solid/white rules. */
.sfp-interact.is-answered:not(.is-expanded) .sfp-chat-toggle,
.sfp-interact.is-awaiting-leader:not(.is-expanded) .sfp-chat-toggle {
  background: var(--sfp-indigo-light);
  color: var(--bs-body-color, #1f2329);
}
.sfp-interact.is-answered:not(.is-expanded) .sfp-chat-toggle:focus-visible,
.sfp-interact.is-awaiting-leader:not(.is-expanded) .sfp-chat-toggle:focus-visible {
  background: rgba(133, 64, 245, 0.16);
}
@media (hover: hover) {
  .sfp-interact.is-answered:not(.is-expanded) .sfp-chat-toggle:hover,
  .sfp-interact.is-awaiting-leader:not(.is-expanded) .sfp-chat-toggle:hover {
    background: rgba(133, 64, 245, 0.16);
  }
}
.sfp-interact.is-answered:not(.is-expanded) .sfp-chat-question,
.sfp-interact.is-awaiting-leader:not(.is-expanded) .sfp-chat-question,
.sfp-interact.is-answered:not(.is-expanded) .sfp-chat-state-text,
.sfp-interact.is-awaiting-leader:not(.is-expanded) .sfp-chat-state-text,
.sfp-interact.is-answered:not(.is-expanded) .sfp-chat-state-prompt,
.sfp-interact.is-awaiting-leader:not(.is-expanded) .sfp-chat-state-prompt {
  color: var(--bs-body-color, #1f2329);
}
.sfp-interact.is-answered:not(.is-expanded) .sfp-chat-toggle-icon,
.sfp-interact.is-awaiting-leader:not(.is-expanded) .sfp-chat-toggle-icon,
.sfp-interact.is-answered:not(.is-expanded) .sfp-chat-toggle-chevron,
.sfp-interact.is-awaiting-leader:not(.is-expanded) .sfp-chat-toggle-chevron {
  color: var(--sfp-indigo);
}
.sfp-interact.is-answered:not(.is-expanded) .sfp-chat-state-icon {
  color: var(--sfp-answered);
}
.sfp-chat-body {
  padding: 8px 12px 12px;
  border-top: 1px solid #eceff2;
  background: #f8f9fa;  /* v7.72.9 — clean neutral chat wall (pilot look) */
}
.sfp-chat-stream {
  /* v12.0.3 (issue 2) — the chat stream no longer has its own inner scrollbar.
     Previously it capped at 320px (35dvh on mobile) with overflow-y:auto, which
     turned the conversation into a small embedded scroll region that was awkward
     to scroll, especially on mobile. The full conversation now renders inline and
     the whole page scrolls — a single, obvious scroll surface inside the
     challenge. participant-chat.js scrolls the newest message into view (page
     scroll) when the reader is already near the bottom. */
  padding: 4px 0;
}
.sfp-chat-row {
  display: flex;
  margin: 5px 0;
  align-items: flex-end;
}
.sfp-chat-row.own { justify-content: flex-end; }
.sfp-chat-row.other { justify-content: flex-start; }
.sfp-chat-bubble {
  /* v12.0.3 — the width CAP now lives on .sfp-chat-bubble-wrap (a flex child of
     the full-width .sfp-chat-row, so its percentage resolves against a DEFINITE
     width). Here the bubble simply fills the wrap up to 100%. Previously this was
     `max-width: 75%`, but the wrap is `inline-flex` (shrink-to-fit / INDEFINITE
     width), so a percentage max-width on the bubble resolved against an indefinite
     containing block and collapsed the bubble to ~1 character — a short message
     like "Ho" wrapped to one letter per line on narrow screens. */
  max-width: 100%;
  padding: 7px 12px 6px;
  border-radius: 16px;
  background: #fff;
  border: 1px solid #e9ecef;
  box-shadow: 0 1px 2px rgba(0,0,0,0.06);
  word-wrap: break-word;
  /* break-word (not anywhere): `anywhere` collapses the bubble's min-content
     width to a single character; break-word keeps the min-width at the longest
     word while still breaking genuinely long unbreakable strings (URLs). */
  overflow-wrap: break-word;
  width: fit-content;
  font-size: 0.9rem;
  line-height: 1.4;
}
/* v8.2.x — three-way bubble palette. Colour rides on the bubble (.is-own /
   .is-leader / .is-other) so it's independent of alignment: in the leader's
   own cockpit their messages are right-aligned AND leader-coloured.
   v14.35.0 (user request) — palette shift: leader/Athena moves from brand
   blue to the platform's question-identity INDIGO (--sfp-indigo #8540f5);
   the participant's own bubble takes the vacated brand blue (was green).
   Other participants stay neutral grey. The leader modal
   (responses-modal.js) mirrors these exact hex values. */
.sfp-chat-bubble.is-own {
  background: #0DA5F2;
  border-color: #0DA5F2;
  color: #fff;
}
.sfp-chat-bubble.is-leader {
  background: #8540f5;
  border-color: #8540f5;
  color: #fff;
}
.sfp-chat-bubble.is-other {
  background: #e9ecef;
  border-color: #e1e4e8;
  color: #212529;
}
.sfp-chat-bubble.is-own a,
.sfp-chat-bubble.is-leader a { color: #fff; text-decoration: underline; }
/* v14.41.0 (user report 3e) — a leader GRADE, in the thread chronology.
   Light green so an evaluation reads as its own kind of event next to the
   leader's ordinary purple reply bubble, without shouting. Dark text (the
   pale ground needs it) and a matching link colour, since .is-grade wins on
   source order over the .is-leader rules above. Only the graded participant
   is ever sent one of these rows. */
.sfp-chat-bubble.is-leader.is-grade {
  background: #d1e7dd;
  border-color: #a3cfbb;
  color: #0a3622;
}
.sfp-chat-bubble.is-leader.is-grade a { color: #0a3622; text-decoration: underline; }
/* v14.55.0 (participant report 4) — the grade bubble's NAME and TIMESTAMP.
   The generic .is-leader rules paint those white (right for the purple
   reply bubble, invisible-ish on this pale green one); the .is-grade
   override recoloured the body text but forgot the meta line. Blue per the
   operator's ruling — link-blue, which already reads as "leader" here. */
/* v14.56.0 (participant report 1) — the v14.55 rule targeted
   .sfp-chat-author, a class that does not exist: the sender renders as
   .sfp-chat-sender (participant-chat.js:437). Caught by the operator; the
   timestamp went blue, the name stayed white. Both selectors kept so the
   pair reads as one intent. */
.sfp-chat-bubble.is-leader.is-grade .sfp-chat-sender,
.sfp-chat-bubble.is-leader.is-grade .sfp-chat-author,
.sfp-chat-bubble.is-leader.is-grade .sfp-chat-time { color: #0a58ca; }
/* v14.44.0 (BACKLOG W5) — the EVALUATION label. The grade bubble's colour is
   no longer its only signal that a message is an evaluation: this tag says so
   in words, so the meaning survives colour-vision deficiency (the pale-green
   grade bubble sits at 1.09:1 against the pale-grey bubble of another
   participant — same lightness, hue-only difference) and reaches screen
   readers, which previously got nothing at all. Bubble's own dark-green ink,
   so it reads as part of the bubble rather than a badge stuck on it. */
.sfp-chat-bubble.is-grade .sfp-chat-grade-tag {
  display: block;
  font-size: 0.72rem;
  font-weight: 700;
  letter-spacing: 0.02em;
  text-transform: uppercase;
  color: #0a3622;
  margin-bottom: 3px;
}
/* Tails follow alignment (own → bottom-right, everyone else → bottom-left). */
.sfp-chat-row.own .sfp-chat-bubble { border-bottom-right-radius: 4px; }
.sfp-chat-row.other .sfp-chat-bubble { border-bottom-left-radius: 4px; }
.sfp-chat-sender {
  font-size: 0.72rem;
  font-weight: 600;
  color: #495057;
  margin-bottom: 2px;
  display: block;
}
/* On the blue leader bubble the sender alias sits in white-ish text. */
.sfp-chat-bubble.is-leader .sfp-chat-sender { color: rgba(255,255,255,0.92); }
.sfp-chat-time {
  font-size: 0.68rem;
  color: #8a9099;
  display: block;
  text-align: right;
  margin-top: 3px;
  white-space: nowrap;
}
.sfp-chat-bubble.is-own .sfp-chat-time,
.sfp-chat-bubble.is-leader .sfp-chat-time { color: rgba(255,255,255,0.8); }
/* v8.2.x — submission bubbles: the chosen options render as chips with an
   optional image thumbnail. Chips are translucent so they read on the
   coloured (own/leader) bubble. The thumbnail opens the same fullscreen
   zoom overlay as content/option images (data-action="open-zoom"). */
.sfp-chat-choices {
  display: flex;
  flex-wrap: wrap;
  gap: 3px 10px;
  margin: 1px 0;
}
/* v8.3.4 (item 2.4) — MC submissions read as ordinary chat bubbles: the chosen
   labels are plain text (no chip background/pill), matching a normal message
   from that sender. Image-choice thumbnails are kept. */
.sfp-chat-choice {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  color: inherit;
  font-size: inherit;
}
.sfp-chat-choice:has(.sfp-chat-choice-thumb) { padding-left: 0; }
.sfp-chat-choice-thumb {
  width: 30px;
  height: 30px;
  object-fit: cover;
  border-radius: 6px;
  cursor: zoom-in;
  display: block;
}
.sfp-chat-empty {
  text-align: center;
  color: #6c757d;
  font-style: italic;
  font-size: 0.85rem;
  padding: 16px 8px;
}
.sfp-chat-error {
  background: rgba(220,53,69,0.08);
  border: 1px solid rgba(220,53,69,0.3);
  border-radius: 4px;
  color: #842029;
  font-size: 0.8rem;
  padding: 6px 10px;
  margin-bottom: 8px;
}
.sfp-chat-composer {
  display: flex;
  gap: 6px;
  align-items: flex-end;
  margin-top: 8px;
  /* v14.40.0 (user proposal) — keep the input + send button pinned to the
     BOTTOM OF THE VIEWPORT with a small margin while the thread is longer
     than the screen, instead of chasing it with scroll maths. `sticky`
     never moves the scroll position, so it cannot fight a participant who
     has scrolled up to read — it just keeps the place-to-type in reach.
     The backdrop stops bubbles showing through as they slide underneath. */
  position: sticky;
  bottom: var(--sfp-composer-gap, 8px);
  z-index: 3;
  background: var(--sfp-chat-bg, #fff);
  padding-top: 6px;
  padding-bottom: 2px;
}
/* The same treatment for the states that REPLACE the composer, so the
   bottom of the thread always holds the same "what can I do here" slot. */
.sfp-chat-waiting,
.sfp-chat-locked {
  position: sticky;
  bottom: var(--sfp-composer-gap, 8px);
  z-index: 3;
  background: var(--sfp-chat-bg, #fff);
  padding-top: 6px;
}
/* v14.40.0 — the locked notice (leader closed the thread). Before this the
   composer rendered NOTHING when closed, so a locked thread looked broken:
   no input, no explanation. */
.sfp-chat-locked {
  display: flex;
  align-items: center;
  gap: 6px;
  font-size: 0.82rem;
  color: #6c757d;
  border-top: 1px solid rgba(0,0,0,0.06);
  margin-top: 8px;
  padding: 8px 2px 4px;
}
.sfp-chat-locked .bi { color: #adb5bd; }

/* v14.40.0 (user request) — "new messages" pill. Shown only when fresh
   bubbles land BELOW the fold while the participant has scrolled away, so
   nothing is silently missed without yanking their scroll position. */
.sfp-chat-newpill {
  position: sticky;
  bottom: calc(var(--sfp-composer-gap, 8px) + 54px); /* v14.49.0 — rides above the raised composer */
  z-index: 4;
  display: block;
  margin: 0 auto 4px;
  width: max-content;
  border: 1px solid #e0a800;
  background: #fff3cd;
  color: #664d03;
  font-size: 0.76rem;
  font-weight: 600;
  border-radius: 999px;
  padding: 3px 12px;
  cursor: pointer;
  box-shadow: 0 2px 6px rgba(0,0,0,0.12);
}
.sfp-chat-newpill:hover { background: #ffe69c; }
.sfp-chat-newpill[hidden] { display: none; }
/* v8.2.0 — out of turn the free-form composer locks alongside the answer
   composer (strict alternation). The disabled inputs grey themselves; this
   just softens the whole row so it reads as intentionally inactive. */
.sfp-chat-composer.is-turn-locked { opacity: 0.75; }
/* v8.3.4 (item 3.1) — shown in place of the composer while awaiting the leader. */
.sfp-chat-waiting {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 8px;
  padding: 14px 12px;
  color: #6c757d;
  font-size: 0.88rem;
  font-style: italic;
}
.sfp-chat-waiting .bi { font-size: 1rem; font-style: normal; }
.sfp-chat-composer textarea {
  flex: 1;
  resize: none;
  border: 1px solid #ced4da;
  border-radius: 18px;
  padding: 8px 14px;
  /* v7.72.9 — 16px (1rem) so iOS Safari does NOT auto-zoom the page when the
     textarea is focused. Any focused input under 16px triggers that zoom, which
     is what made the chat "zoom in a bit" and shifted the layout on tap. */
  font-size: 1rem;
  line-height: 1.4;
  max-height: 120px;
  min-height: 38px;
  background: #fff;
  outline: none;
  transition: border-color 0.15s ease;
}
.sfp-chat-composer textarea:focus {
  border-color: var(--sfp-brand, #0DA5F2);
  box-shadow: 0 0 0 0.15rem rgba(13,165,242,0.18);
}
.sfp-chat-composer textarea:disabled {
  background: #f8f9fa;
  color: #6c757d;
  cursor: not-allowed;
}
.sfp-chat-send {
  flex-shrink: 0;
  width: 38px; height: 38px;
  border-radius: 50%;
  border: 0;
  /* v8.1.0 — use the app brand blue (same as .sfp-submit-btn / btn-primary)
     instead of Bootstrap's #0d6efd, so the chat send matches the answer
     "Save" button. */
  background: var(--sfp-brand, #0DA5F2);
  color: #fff;
  display: flex; align-items: center; justify-content: center;
  cursor: pointer;
  padding: 0;
  transition: background-color 0.15s ease, transform 0.1s ease;
}
.sfp-chat-send:hover { background: var(--sfp-brand-dark, #0884C2); }
.sfp-chat-send:active { transform: scale(0.94); }
.sfp-chat-send:disabled { background: #adb5bd; cursor: not-allowed; }
.sfp-chat-send svg { transform: translateX(1px); }

/* ─── v6.4 — chat polish: avatars, day separators, pulse, grouping ───── */

/* Avatar circle next to "other" bubbles. Own bubbles have no avatar
 * (right-aligned; the absence IS the identity).
 * Spacer keeps streak bubbles aligned under the avatar of the first
 * message in the streak. */
.sfp-chat-avatar,
.sfp-chat-avatar-spacer {
  /* v14.37.0 (user request) — 24px read as a dot; 28px + the 1.3em glyph
     below make the identity mark legible at arm's length. */
  width: 28px;
  height: 28px;
  flex-shrink: 0;
  margin-right: 6px;
  align-self: flex-end;
  margin-bottom: 2px;
}
.sfp-chat-avatar {
  border-radius: 50%;
  background: #6c757d;
  color: #fff;
  font-size: 0.65rem;
  font-weight: 600;
  letter-spacing: 0.02em;
  display: flex;
  align-items: center;
  justify-content: center;
  text-transform: uppercase;
}
.sfp-chat-avatar-spacer { background: transparent; }

/* Streak — second+ message from same sender within 60s.
 * Tighten vertical spacing so the streak reads as a single block. */
.sfp-chat-row.is-streak {
  margin-top: 1px;
}
.sfp-chat-row.is-streak .sfp-chat-bubble {
  /* Slightly tighter top padding when stacking — visually attaches
   * the streak bubble to its predecessor. */
  padding-top: 4px;
}

/* Day separator pill, centered in the stream. */
.sfp-chat-day-sep {
  text-align: center;
  margin: 12px 0 6px;
  position: relative;
}
.sfp-chat-day-sep span {
  display: inline-block;
  background: rgba(255,255,255,0.85);
  border: 1px solid rgba(0,0,0,0.05);
  color: #495057;
  font-size: 0.72rem;
  font-weight: 600;
  padding: 2px 10px;
  border-radius: 10px;
  text-transform: uppercase;
  letter-spacing: 0.04em;
}

/* v14.39.0 (user report #3a) — a round boundary rendered IN the thread. Like
 * the day separator but visually distinct (brand-tinted, a repeat glyph) so a
 * participant can see where each round starts in the conversation history. */
.sfp-chat-round-sep {
  text-align: center;
  margin: 14px 0 8px;
  position: relative;
}
.sfp-chat-round-sep::before {
  content: "";
  position: absolute;
  left: 0; right: 0; top: 50%;
  border-top: 1px dashed rgba(63, 55, 201, 0.25);
  z-index: 0;
}
.sfp-chat-round-sep span {
  position: relative;
  z-index: 1;
  display: inline-block;
  background: #eef0fb;
  border: 1px solid rgba(63, 55, 201, 0.2);
  color: #3f37c9;
  font-size: 0.72rem;
  font-weight: 700;
  padding: 2px 12px;
  border-radius: 10px;
  text-transform: uppercase;
  letter-spacing: 0.04em;
}
.sfp-chat-round-sep span .bi { font-size: 0.9em; margin-right: 2px; }

/* Pulse on the unread badge when count increments. Plays once,
 * triggered by toggling .is-pulsing in JS with a reflow flush. */
@keyframes sfp-chat-badge-pulse {
  0%   { transform: scale(1);    box-shadow: 0 0 0 0 rgba(220,53,69,0.6); }
  50%  { transform: scale(1.18); box-shadow: 0 0 0 6px rgba(220,53,69,0); }
  100% { transform: scale(1);    box-shadow: 0 0 0 0 rgba(220,53,69,0); }
}
.sfp-chat-toggle-badge.is-pulsing {
  animation: sfp-chat-badge-pulse 0.7s ease-out;
}

/* Respect reduced-motion preference — no animation if asked. */
@media (prefers-reduced-motion: reduce) {
  .sfp-chat-toggle-badge.is-pulsing {
    animation: none;
  }
}

/* ─── v6.4 leader modal — same avatars + day separators ─────────────── */

.responses-avatar,
.responses-avatar-spacer {
  width: 24px;
  height: 24px;
  flex-shrink: 0;
  margin-right: 6px;
  align-self: flex-end;
  margin-bottom: 2px;
}
.responses-avatar {
  border-radius: 50%;
  background: #6c757d;
  color: #fff;
  font-size: 0.65rem;
  font-weight: 600;
  display: flex;
  align-items: center;
  justify-content: center;
  text-transform: uppercase;
}
.responses-avatar-spacer { background: transparent; }
.responses-bubble-row.is-streak { margin-top: 1px; }
.responses-day-sep {
  text-align: center;
  margin: 10px 0 4px;
}
.responses-day-sep span {
  display: inline-block;
  background: rgba(255,255,255,0.85);
  border: 1px solid rgba(0,0,0,0.05);
  color: #495057;
  font-size: 0.72rem;
  font-weight: 600;
  padding: 2px 10px;
  border-radius: 10px;
  text-transform: uppercase;
  letter-spacing: 0.04em;
}

/* v14.46.0 (D2) — the leader thread's ROUND boundary. Deliberately the same
   indigo dashed rule as the participant thread's .sfp-chat-round-sep: the two
   sides render the same events, and a leader comparing their screen with a
   participant's should see one product, not two conventions. It reads heavier
   than the day separator above on purpose — a new round is a change of
   question, a new day is only a passage of time. */
.responses-round-sep {
  text-align: center;
  margin: 14px 0 8px;
  position: relative;
}
.responses-round-sep::before {
  content: "";
  position: absolute;
  left: 0; right: 0; top: 50%;
  border-top: 1px dashed rgba(63, 55, 201, 0.25);
  z-index: 0;
}
.responses-round-sep span {
  position: relative;
  z-index: 1;
  display: inline-block;
  background: #eef0fb;
  border: 1px solid rgba(63, 55, 201, 0.2);
  color: #3f37c9;
  font-size: 0.72rem;
  font-weight: 700;
  padding: 2px 12px;
  border-radius: 10px;
  text-transform: uppercase;
  letter-spacing: 0.04em;
}
.responses-round-sep span .bi { font-size: 0.9em; margin-right: 2px; }

/* ─── v6.7 — Chart element (bar visualization) ─────────────────────────
 * Per options-charts-design-v6 §6.1. Pure HTML/CSS bar chart — no
 * Chart.js dependency. Each row is a label + a horizontal track with
 * a fill bar + the formatted value. Transitions on width changes
 * animate value updates.
 */
.sfp-chart-element {
  background: #fff;
  border: 1px solid #e4e4e4;
  border-radius: 8px;
  padding: 14px 16px;
}
.sfp-chart-title {
  margin: 0 0 12px;
  font-size: 1rem;
  font-weight: 600;
  color: #212529;
}
.sfp-chart-loading {
  color: #6c757d;
  font-style: italic;
  padding: 12px 0;
  text-align: center;
}
.sfp-chart-empty {
  color: #6c757d;
  font-style: italic;
  padding: 18px 0;
  text-align: center;
}
.sfp-chart-rows {
  display: flex; flex-direction: column;
  gap: 8px;
}
.sfp-chart-row {
  display: grid;
  grid-template-columns: minmax(80px, 25%) 1fr;
  gap: 12px;
  align-items: center;
}
.sfp-chart-row-label {
  font-size: 0.875rem;
  color: #495057;
  font-weight: 500;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.sfp-chart-partial {
  font-size: 0.7rem;
  color: #fd7e14;
  margin-left: 4px;
  background: rgba(253,126,20,0.1);
  padding: 1px 5px;
  border-radius: 3px;
  font-weight: 600;
}
.sfp-chart-row-track {
  position: relative;
  height: 26px;
  background: #f1f3f5;
  border-radius: 4px;
  display: flex;
  align-items: center;
}
.sfp-chart-row-bar {
  background: linear-gradient(90deg, #0d6efd, #0a58ca);
  height: 100%;
  border-radius: 4px;
  transition: width 0.45s cubic-bezier(0.4, 0.0, 0.2, 1);
  min-width: 2px;
}
.sfp-chart-row-value {
  position: absolute;
  right: 8px;
  font-size: 0.85rem;
  font-weight: 600;
  color: #212529;
  text-shadow: 0 1px 2px rgba(255,255,255,0.7);
}
.sfp-chart-overall {
  margin-top: 12px;
  padding-top: 10px;
  border-top: 1px solid #e9ecef;
  font-size: 0.85rem;
  color: #495057;
}
/* v6.44 — T5.2: two-bars-per-group (by_source series). The
   participant bar keeps the default blue; the leader bar uses a
   distinct amber so the two are tellable apart at a glance. A small
   source tag labels each line. */
.sfp-chart-row--split .sfp-chart-split-bars {
  display: flex;
  flex-direction: column;
  gap: 4px;
}
.sfp-chart-split-line {
  display: flex;
  align-items: center;
  gap: 6px;
}
.sfp-chart-split-tag {
  flex: 0 0 auto;
  font-size: 0.65rem;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.02em;
  width: 64px;
  color: #6c757d;
}
.sfp-chart-split-tag--p { color: #0a58ca; }
.sfp-chart-split-tag--l { color: #b8860b; }
.sfp-chart-split-line .sfp-chart-row-track { flex: 1 1 auto; }
.sfp-chart-row-bar--p {
  background: linear-gradient(90deg, #0d6efd, #0a58ca);
}
.sfp-chart-row-bar--l {
  background: linear-gradient(90deg, #ffc107, #d39e00);
}
/* v7.70.3 (S22) — negative metric values. Bar length is magnitude (the
   scale uses Math.abs), so a negative value would otherwise look identical
   to a positive one of the same size; the red fill + the signed value label
   make the direction unmistakable. Listed after the blue/amber rules so it
   wins when combined as `sfp-chart-row-bar--p sfp-chart-row-bar--negative`. */
.sfp-chart-row-bar--negative,
.sfp-chart-row-bar--p.sfp-chart-row-bar--negative,
.sfp-chart-row-bar--l.sfp-chart-row-bar--negative {
  background: linear-gradient(90deg, #dc3545, #b02a37);
}
.sfp-chart-overall.sfp-chart-overall-quiet {
  color: #6c757d;
  font-style: italic;
  font-size: 0.8rem;
}
@media (prefers-reduced-motion: reduce) {
  .sfp-chart-row-bar { transition: none; }
}

/* ─── v12.0.56 — leader full-screen chart modal ────────────────────────
 * Opened from the visible-elements list's chart button. The chart inside is a
 * data-chart-side="leader" shell rendered by chart-element.js; the gear toggles
 * the view controls (which chart-element.js renders into [data-sfp-chart-controls]).
 * Brand-blue accents (--bs-primary) match the chart element colour. */
.sfp-chartmodal-overlay {
  position: fixed; inset: 0; z-index: 1085;
  background: rgba(0,0,0,0.55);
  display: flex; align-items: center; justify-content: center;
  padding: 16px;
}
.sfp-chartmodal {
  background: #fff; border-radius: 12px;
  width: min(1000px, 96vw); max-height: 92vh;
  display: flex; flex-direction: column;
  box-shadow: 0 12px 48px rgba(0,0,0,0.3);
  overflow: hidden;
}
.sfp-chartmodal-head {
  display: flex; align-items: center; gap: 8px;
  padding: 12px 16px; border-bottom: 1px solid #e9ecef;
  flex-shrink: 0;
}
.sfp-chartmodal-title {
  flex: 1 1 auto; min-width: 0;
  font-size: 1.05rem; font-weight: 600; color: #212529;
  /* v12.0.276 (FR3.17) — a 2-line clamp instead of the old single-line
     ellipsis; clicking toggles the full title (see chart-modal.js). */
  display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical;
  overflow: hidden; cursor: pointer;
}
.sfp-chartmodal-title.is-expanded { display: block; -webkit-line-clamp: unset; }
.sfp-chartmodal-gear,
.sfp-chartmodal-close {
  flex-shrink: 0; width: 34px; height: 34px;
  display: inline-flex; align-items: center; justify-content: center;
  border: 1px solid #ced4da; background: #fff; color: #495057;
  border-radius: 8px; cursor: pointer;
  transition: background-color .15s, border-color .15s, color .15s;
}
.sfp-chartmodal-gear:hover,
.sfp-chartmodal-close:hover {
  background: #e1f1fc; border-color: var(--bs-primary, #0d6efd); color: #212529;
}
.sfp-chartmodal-gear.is-open {
  background: var(--bs-primary, #0d6efd); border-color: var(--bs-primary, #0d6efd); color: #fff;
}
.sfp-chartmodal-gear i { transition: transform .2s; }
.sfp-chartmodal-gear.is-open i { transform: rotate(60deg); }
.sfp-chartmodal-body {
  padding: 16px; overflow-y: auto;
}
.sfp-chartmodal-body .sfp-chart-element { border: 0; padding: 0; }
.sfp-chartmodal-settings {
  margin-bottom: 14px; padding: 12px 14px;
  background: #f8f9fa; border: 1px solid #e9ecef; border-radius: 8px;
}

/* ─── v6.8 — Toast stack (bottom-right) ────────────────────────────────
 * Per responses-system-design-v12 §7.6 — two-zone system messages.
 * Toasts stack at the bottom-right; pill notifications live elsewhere
 * (online/offline state, etc).
 */
.sfp-toast-stack {
  position: fixed;
  bottom: 20px;
  right: 20px;
  z-index: 9999;
  display: flex;
  flex-direction: column;
  gap: 8px;
  pointer-events: none;
  max-width: min(360px, calc(100vw - 40px));
}
.sfp-toast {
  background: #212529;
  color: #fff;
  padding: 10px 14px;
  border-radius: 6px;
  font-size: 0.875rem;
  line-height: 1.4;
  box-shadow: 0 4px 12px rgba(0,0,0,0.25);
  display: flex;
  align-items: flex-start;
  gap: 10px;
  pointer-events: auto;
  animation: sfp-toast-in 0.2s ease-out;
}
.sfp-toast.is-leaving {
  opacity: 0;
  transform: translateX(20px);
  transition: opacity 0.2s, transform 0.2s;
}
.sfp-toast--success { background: #198754; }
.sfp-toast--warn    { background: #fd7e14; }
.sfp-toast--error   { background: #dc3545; }
.sfp-toast-msg { flex: 1; }
.sfp-toast-close {
  background: transparent;
  border: 0;
  color: rgba(255,255,255,0.8);
  font-size: 1.1rem;
  line-height: 1;
  padding: 0 0 0 6px;
  cursor: pointer;
  font-weight: 600;
}
.sfp-toast-close:hover { color: #fff; }
@keyframes sfp-toast-in {
  from { opacity: 0; transform: translateX(20px); }
  to   { opacity: 1; transform: translateX(0); }
}
@media (prefers-reduced-motion: reduce) {
  .sfp-toast { animation: none; }
  .sfp-toast.is-leaving { transition: opacity 0.05s; transform: none; }
}

/* v6.8 — Privacy notice on participant chat (design v12 §9.4). */
/* v12.0.225 — the in-body .sfp-chat-privacy notice was replaced by the header
   .sfp-chat-privacy-inline note (see the state-text block); the old rule is gone. */

/* ─── v6.10 — Edit/delete menu + send-state indicators ─────────────────
 * Per WhatsApp polish backlog P1. Own bubbles within 5min of sending
 * carry a small ⋯ menu trigger; clicking opens a floating menu with
 * Edit / Delete actions. The status icon next to the timestamp shows
 * sending (⏱) or sent (✓) — analogous to WhatsApp's single-tick.
 */
.sfp-chat-bubble-wrap {
  position: relative;
  display: inline-flex;
  align-items: flex-start;
  /* v12.0.3 — the bubble width cap lives HERE, on a flex child of the
     full-width .sfp-chat-row, so the percentage resolves against a definite
     width (the row). 80% leaves a little room for the ⋯ menu button that sits
     inline on own messages within the edit window. */
  max-width: 80%;
  min-width: 0;
}
/* v14.59.3 (report 10) — the wrap is an inline-FLEX ROW, so when a choice
   answer produces TWO bubbles (v12.0.403 puts the chosen image in its own
   bubble above the label text) they were laid out SIDE BY SIDE instead of as
   two chronological messages. The markup was already correct — image bubble
   first, text bubble second — only the axis was wrong. Column now, aligned to
   the message's own side, and the row-reverse for own messages becomes
   column-reverse-free: the ⋯ menu keeps its place via align-self. */
.sfp-chat-bubble-wrap {
  flex-direction: column;
  align-items: flex-start;
  gap: 0.25rem;
}
.sfp-chat-row.own .sfp-chat-bubble-wrap {
  align-items: flex-end;
}
.sfp-chat-row.own .sfp-chat-msg-menu-btn { align-self: flex-end; }
.sfp-chat-msg-menu-btn {
  background: transparent;
  border: 0;
  cursor: pointer;
  color: rgba(0,0,0,0.4);
  font-size: 1.1rem;
  line-height: 1;
  padding: 2px 4px;
  margin: 4px 4px 0;
  border-radius: 4px;
  opacity: 0;
  transition: opacity 0.15s, background-color 0.15s;
  align-self: flex-start;
}
.sfp-chat-row:hover .sfp-chat-msg-menu-btn,
.sfp-chat-msg-menu-btn:focus {
  opacity: 1;
}
.sfp-chat-msg-menu-btn:hover {
  background: rgba(0,0,0,0.07);
  color: rgba(0,0,0,0.7);
}
.sfp-chat-msg-menu {
  position: absolute;
  z-index: 10000;
  background: #fff;
  border: 1px solid #dee2e6;
  border-radius: 6px;
  box-shadow: 0 6px 20px rgba(0,0,0,0.15);
  padding: 4px;
  min-width: 140px;
  display: flex;
  flex-direction: column;
}
.sfp-chat-msg-menu button {
  background: transparent;
  border: 0;
  text-align: left;
  padding: 8px 10px;
  border-radius: 4px;
  font-size: 0.875rem;
  cursor: pointer;
  color: #212529;
  display: flex;
  align-items: center;
  gap: 8px;
}
.sfp-chat-msg-menu button:hover,
.sfp-chat-msg-menu button:focus {
  background: #f1f3f5;
  outline: none;
}
.sfp-chat-msg-menu button[data-sfp-msg-action="delete"]:hover {
  background: #fff5f5;
  color: #dc3545;
}
.sfp-chat-edited {
  font-style: italic;
  color: rgba(0,0,0,0.45);
  font-size: 0.7rem;
  margin-left: 4px;
}
.sfp-chat-row.own .sfp-chat-edited {
  color: rgba(255,255,255,0.7);
}
.sfp-chat-status {
  margin-left: 4px;
  font-size: 0.85rem;
  vertical-align: baseline;
}
.sfp-chat-status-sending {
  opacity: 0.6;
}
.sfp-chat-status-sent {
  color: rgba(255,255,255,0.85);
}
.sfp-chat-row.other .sfp-chat-status-sent {
  color: rgba(0,0,0,0.5);
}
/* v12.0.75 (item 5) — the optimistic answer bubble: a quick confirmation
   pop-in, then a gentle "updating" pulse on the sending indicator until the
   real server submission arrives and replaces this local-* row. */
.sfp-answer-pending .sfp-chat-bubble {
  animation: sfp-answer-popin 0.22s ease-out;
}
.sfp-answer-pending .sfp-chat-status-sending {
  animation: sfp-answer-sending-pulse 1.1s ease-in-out infinite;
}
@keyframes sfp-answer-popin {
  from { transform: scale(0.94); opacity: 0; }
  to   { transform: scale(1);    opacity: 1; }
}
@keyframes sfp-answer-sending-pulse {
  0%, 100% { opacity: 0.45; }
  50%      { opacity: 1; }
}
@media (prefers-reduced-motion: reduce) {
  .sfp-answer-pending .sfp-chat-bubble,
  .sfp-answer-pending .sfp-chat-status-sending { animation: none; }
}
/* Touch: ⋯ button always visible on touch devices (no hover) */
@media (hover: none) {
  .sfp-chat-msg-menu-btn { opacity: 0.65; }
}

/* v6.48 — 1.5 (Q4): participant-facing navigation is BLUE, not the
   near-black bg-dark used by the leader/admin side. This visually
   distinguishes "where participants are" from "where leaders work",
   matching the participant=blue convention used in the editor's
   per-side sections. Applied to the participant sidebar's mobile
   navbar, offcanvas, and desktop column. data-bs-theme="dark" stays
   so text, the toggler icon, and the close button remain light for
   contrast against the blue. */
.sf-nav-participant {
  /* v14.59.0 (report 7a) — was #0a4d8c (blue). The participant sidebar now
     matches the leader's dark chrome; Bootstrap's $dark, so the existing
     text-white / text-white-50 labels keep their contrast. */
  background-color: #212529 !important;
}
/* The desktop column's internal divider uses border-secondary (a grey
   tuned for bg-dark); on blue a translucent white reads cleaner. */
.sf-sidebar.sf-nav-participant .border-bottom {
  border-color: rgba(255, 255, 255, 0.18) !important;
}

/* v6.49 — 1.5: editor per-side sections (replace the v6.35 tabs).
   Two stacked collapsible sections per interact element — participant
   (blue) and leader (black) — each with an activating checkbox in its
   header. Accordion: at most one body open at a time. */
.sf-side-section {
  border: 1px solid #dee2e6;
  border-radius: 6px;
  overflow: hidden;
}
.sf-side-section .sf-side-header {
  padding: 6px 10px;
}
.sf-side-headbtn {
  color: inherit !important;
  font-size: 0.9rem;
}
.sf-side-headbtn:hover { color: inherit; }
.sf-side-chev { color: var(--bs-secondary-color); display: inline-flex; align-items: center; }
.sf-side-body {
  padding: 8px 10px 10px;
  border-top: 1px solid #e9ecef;
}
/* Participant = blue */
.sf-side-participant .sf-side-header {
  background-color: #e7f1ff;
  border-left: 4px solid #0a4d8c;
}
.sf-side-participant .sf-side-headbtn { color: #0a4d8c !important; }
/* Leader = black */
.sf-side-leader .sf-side-header {
  background-color: #ececed;
  border-left: 4px solid #212529;
}
.sf-side-leader .sf-side-headbtn { color: #212529 !important; }

/* v6.52 — T5.4b: label-distribution chart. A legend maps colours to
   label values; each group is a horizontal stacked bar of coloured
   segments (one per label value). Segment width is absolute (count /
   max group total) so bar length conveys volume and the colour split
   conveys composition. */
.sfp-chart-legend {
  display: flex;
  flex-wrap: wrap;
  gap: 6px 14px;
  margin-bottom: 12px;
  font-size: 0.8rem;
  color: #495057;
}
.sfp-chart-legend-item { display: inline-flex; align-items: center; }
.sfp-chart-legend-swatch {
  width: 12px; height: 12px;
  border-radius: 3px;
  margin-right: 5px;
  flex: 0 0 auto;
}
.sfp-chart-legend-count { color: #868e96; }
.sfp-chart-row-track--stack {
  overflow: hidden;
  gap: 1px;
}
.sfp-chart-seg {
  height: 100%;
  min-width: 2px;
  transition: width 0.45s cubic-bezier(0.4, 0.0, 0.2, 1);
}
.sfp-chart-seg:first-child { border-top-left-radius: 4px; border-bottom-left-radius: 4px; }
.sfp-chart-seg:last-child { border-top-right-radius: 4px; border-bottom-right-radius: 4px; }
.sfp-chart-row--label .sfp-chart-row-value {
  position: static;
  margin-left: 8px;
  min-width: 1.5em;
  text-align: right;
}
.sfp-chart-row--label { display: flex; align-items: center; gap: 8px; }
.sfp-chart-row--label .sfp-chart-row-track { flex: 1 1 auto; }

/* v6.53 — T5.3: table visualization for charts. Compact, scrollable on
   narrow screens, with a quiet header + zebra body. Works for both the
   metric (Group | Value | Responses) and label (frequency matrix)
   layouts. */
.sfp-chart-tablewrap { overflow-x: auto; }
.sfp-chart-table {
  width: 100%;
  border-collapse: collapse;
  font-size: 0.85rem;
}
.sfp-chart-table th,
.sfp-chart-table td {
  padding: 6px 10px;
  border-bottom: 1px solid #e9ecef;
  white-space: nowrap;
}
.sfp-chart-table thead th {
  color: #495057;
  font-weight: 600;
  border-bottom: 2px solid #dee2e6;
}
.sfp-chart-table tbody tr:nth-child(even) { background: #f8f9fa; }
.sfp-chart-table th[scope="row"] { font-weight: 500; text-align: left; }
.sfp-chart-table tfoot th,
.sfp-chart-table tfoot td {
  border-top: 2px solid #dee2e6;
  border-bottom: none;
  color: #495057;
}
.sfp-chart-table .text-end { text-align: right; }

/* v6.54 — T5.3b: pie visualization. Pure SVG pie + the shared legend.
   The pie is capped at a comfortable size and centred; the legend wraps
   beneath it on narrow screens and beside it when there's room. */
.sfp-chart-pie-wrap {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 16px;
}
.sfp-chart-pie {
  width: 160px;
  height: 160px;
  flex: 0 0 auto;
}
.sfp-chart-pie-wrap .sfp-chart-legend {
  margin-bottom: 0;
  flex-direction: column;
  gap: 4px;
}

/* ── v7.72.7 — admin Exercise-tab upload drop-zone (item 4) ──────────────────
   The primary upload affordance. Pairs with the sub-nav switching from filled
   pills to underline tabs so the tabs no longer read as the button to press.
   Uses Bootstrap CSS vars so it follows light/dark theme. */
.ex-dropzone {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 0.35rem;
  width: 100%;
  padding: 1.25rem 1rem;
  border: 2px dashed var(--bs-border-color, #ced4da);
  border-radius: 0.5rem;
  background: var(--bs-tertiary-bg, #f8f9fa);
  color: var(--bs-body-color, #212529);
  text-align: center;
  cursor: pointer;
  transition: border-color .15s ease, background-color .15s ease;
  margin-bottom: 0;
}
.ex-dropzone:hover,
.ex-dropzone:focus-within {
  border-color: var(--bs-primary, #0d6efd);
  background: rgba(13, 110, 253, 0.05);
}
.ex-dropzone.is-dragover {
  border-color: var(--bs-primary, #0d6efd);
  background: rgba(13, 110, 253, 0.10);
}
.ex-dropzone-icon {
  font-size: 1.75rem;
  color: var(--bs-primary, #0d6efd);
  line-height: 1;
}
.ex-dropzone-text { font-size: 0.9rem; }
.ex-dropzone-browse {
  color: var(--bs-primary, #0d6efd);
  text-decoration: underline;
  font-weight: 600;
}
.ex-dropzone-filename {
  display: inline-block;
  font-size: 0.8rem;
  font-family: var(--bs-font-monospace, monospace);
  color: var(--bs-body-color);
  background: var(--bs-body-bg, #fff);
  border: 1px solid var(--bs-border-color, #ced4da);
  border-radius: 0.25rem;
  padding: 0.1rem 0.4rem;
  margin-top: 0.15rem;
  max-width: 100%;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* ============================================================
   v7.72.10 — Phase 4: options inside the chat.
   The interact element is now a single chat-centric card: the
   toggle bar carries the question + an answer-state hint, and the
   expanded panel leads with the answer card (the focal task), then
   the discussion thread. See _participant-elements.ejs. The
   .sfp-choices / .sfp-choice / .sfp-textarea / .sfp-submit-* rules
   above are reused as-is inside the answer card.
   ============================================================ */

/* Toggle bar — stack the question over the state hint; align the
   rocket / badge / chevron to the question's first line. */
.sfp-chat-toggle { align-items: flex-start; }
.sfp-interact .sfp-chat-toggle-icon { margin-top: 1px; }
.sfp-interact .sfp-chat-toggle-chevron { margin-top: 3px; }
.sfp-interact .sfp-chat-toggle-badge { margin-top: 2px; }
.sfp-chat-toggle-main {
  flex: 1;
  display: flex;
  flex-direction: column;
  gap: 2px;
  min-width: 0;          /* let the state hint truncate, not overflow */
}
.sfp-chat-question {
  font-weight: 600;
  font-size: 0.95rem;
  color: #1a1d21;
  line-height: 1.4;
}
/* v12.0.252 — the My progress page */
.sfp-back-link { color: #6c5ba7; font-size: .85rem; text-decoration: none; }
.sfp-back-link:hover { text-decoration: underline; }
.sfp-progress-card {
  background: #fff; border: 1px solid #e3e0f0; border-radius: 12px;
  padding: 16px 18px; max-width: 480px;
}
.sfp-progress-level { display: flex; align-items: baseline; gap: 12px; margin-bottom: 10px; }
.sfp-progress-level-badge {
  display: inline-flex; align-items: center; gap: 7px;
  font-size: 1.15rem; font-weight: 700; color: #5a3d8a;
}
.sfp-progress-level-badge .bi { color: #f0b429; }
.sfp-progress-total { color: #444; font-weight: 600; font-variant-numeric: tabular-nums; }
.sfp-progress-bar { height: 8px; border-radius: 999px; background: #efeaf9; }
.sfp-progress-bar .progress-bar { background: #7a5fd0; border-radius: 999px; }
.sfp-progress-tonext { margin-top: 5px; }
.sfp-progress-breakdown { margin-top: 12px; display: flex; flex-direction: column; gap: 4px; }
.sfp-progress-breakrow {
  display: flex; align-items: center; gap: 8px; font-size: .88rem;
  padding: 3px 0; color: #333;
}
.sfp-progress-breakrow .bi { color: #8a7ab0; }

/* v12.0.249 (L1) — the lobby XP leaderboard */
.sfp-lobby-board {
  display: inline-block; text-align: left; background: #fff;
  border: 1px solid #e3e0f0; border-radius: 10px; padding: 8px 14px 6px;
  min-width: 240px; max-width: 340px;
}
.sfp-lobby-board-head {
  display: flex; align-items: center; gap: 6px;
  font-size: .78rem; font-weight: 700; color: #5a3d8a; margin-bottom: 4px;
}
.sfp-lobby-board-list { list-style: none; margin: 0; padding: 0; }
.sfp-lobby-board-row, .sfp-lobby-board-merow {
  display: flex; align-items: baseline; gap: 8px;
  font-size: .82rem; padding: 2px 4px; border-radius: 6px;
}
.sfp-lobby-board-row.is-me { background: #f3edff; font-weight: 600; }
.sfp-lobby-board-merow { border-top: 1px dashed #d8d2ea; margin-top: 3px; padding-top: 4px; font-weight: 600; }
.sfp-lobby-board-rank { color: #8a7ab0; font-variant-numeric: tabular-nums; min-width: 2.2em; }
.sfp-lobby-board-name { flex: 1 1 auto; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.sfp-lobby-board-you { color: #7a5fd0; font-weight: 400; }
.sfp-lobby-board-xp { font-variant-numeric: tabular-nums; color: #444; }

/* v12.0.248 (E3) — the participant's own evaluation result badge */
.sfp-eval-result {
  display: inline-flex; align-items: center; gap: 5px;
  font-size: .78rem; color: #1d6b3a; background: #eef8f1;
  border: 1px solid #cfe8d6; border-radius: 999px; padding: 1px 10px;
  margin-right: 8px; white-space: nowrap;
}
.sfp-eval-result strong { font-weight: 700; }
.sfp-eval-result-xp { font-weight: 700; opacity: .8; font-size: .72rem; }

.sfp-chat-state {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  font-size: 0.8rem;
  font-weight: 500;
  min-width: 0;
}
.sfp-chat-state-icon { color: var(--sfp-answered); font-size: 0.85rem; flex-shrink: 0; }

/* v12.0.298 (design/choice-collapse.md) — the bar's option-count hint
   ("Tap to respond · 5 options"): quieter than the prompt it rides on. */
.sfp-chat-optcount { opacity: 0.7; font-weight: 400; }

/* v12.0.299 (review F2) — a link element whose stored url fails the scheme
   guard (pre-v297 snapshot data) renders inert: visible label, no anchor. */
.sfp-link-disabled { pointer-events: none; opacity: 0.6; }

/* v12.0.305 (feedback P1) — long session titles overflowed the sidebar:
   .text-truncate inside a flex row cannot shrink below its content without
   min-width:0 on the flex item (the classic flexbox truncation trap). */
.sf-nav-participant .nav-link { min-width: 0; }
.sf-nav-participant .nav-link > .text-truncate { min-width: 0; flex: 1 1 auto; overflow: hidden; }

/* v12.0.378 (participant feedback 2) — "nothing should overflow the sidebar",
   made structural instead of per-element. The v305 fix covered the direct
   .text-truncate children, but the class kept recurring: any OTHER flex child
   (badges, icons, non-truncate spans), any long unbroken token (a journey
   name pasted without spaces, an away note), and — on phones narrower than
   Bootstrap's 400px default — the OFFCANVAS ITSELF extended past the
   viewport, cutting off the right edge of every row. Four layers:
     1. the offcanvas never exceeds 86vw, so its right edge (badges, counts)
        stays on-screen on any phone;
     2. both sidebar surfaces clip horizontally (overflow-x) as the final
        backstop;
     3. every flex child of a nav row may shrink (min-width:0), with badges
        and icons opting back out (flex:none keeps them readable);
     4. free-text blocks (away note, headings) break long tokens instead of
        pushing the panel wide. */
.sf-nav-participant.offcanvas { --bs-offcanvas-width: min(400px, 86vw); }
.sf-sidebar.sf-nav-participant,
.sf-nav-participant .offcanvas-body { overflow-x: hidden; }
.sf-nav-participant .nav { min-width: 0; max-width: 100%; }
.sf-nav-participant .nav-item { min-width: 0; max-width: 100%; }
.sf-nav-participant .nav-link > * { min-width: 0; }
.sf-nav-participant .nav-link .badge,
.sf-nav-participant .nav-link .bi { flex: 0 0 auto; min-width: auto; }
.sf-nav-participant .nav-item { overflow-wrap: anywhere; }

/* v12.0.305 (feedback P3) — the step-away summary hugs its label. */
.sfp-away-summary { display: inline-block; width: auto; }

.sfp-chat-state-text {
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  color: var(--sfp-answered);
}
.sfp-chat-state-prompt { color: #6c757d; }
/* v12.0.225 (user items 2a + 3) — when the panel is EXPANDED, the collapsed
   state text (e.g. "Tap to respond" — redundant beside the open answer card)
   hides and the visibility note takes its place in the header: who can read
   this thread ("visible to your group members and the leader"). The note is
   server-rendered ([data-sfp-privacy-inline]) and text-swapped by
   participant-chat.js on mid-session reassignment (S33). */
.sfp-interact.is-expanded .sfp-chat-state { display: none; }
.sfp-chat-privacy-inline {
  display: none;
  align-items: center;
  gap: 4px;
  font-size: 0.76rem;
  font-weight: 400;
  color: #6c757d;
  min-width: 0;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.sfp-chat-privacy-inline .bi { font-size: 0.8rem; flex-shrink: 0; opacity: 0.75; }
.sfp-interact.is-expanded .sfp-chat-privacy-inline { display: inline-flex; }
/* v12.0.189 — participant turn-state colours. Amber = the answer is in and the
   ball is with the leader (NOT the green "done" check); indigo + bold = a loud
   "your turn" when the leader has replied. !important so these win over the
   generic answered-green rules that fire on the collapsed/expanded card. */
.sfp-chat-state-icon-waiting { color: #c8860a !important; }
.sfp-chat-state-waiting { color: #8a5a00 !important; }
.sfp-chat-state-icon-yourturn { color: var(--sfp-indigo) !important; }
.sfp-chat-state-yourturn { color: var(--sfp-indigo-dark) !important; font-weight: 700; }
/* v12.0.316 (TC.8, design/leader-activity-signal.md) — answered AND the
   leader replied since the participant last looked: a teal accent on the
   check + label, quieter than yourturn (nothing is required of the
   participant) but distinct from the plain green Answered. */
.sfp-chat-state-text.is-leader-replied,
.sfp-chat-state-icon.is-leader-replied { color: var(--bs-teal, #20c997) !important; font-weight: 600; }
/* v12.0.207 (turn-consistency review, bug 2) — the COLLAPSED toggle is
   indigo-filled with white text; the state colors above were designed for the
   expanded (light) header and were barely readable there (dark-indigo-on-indigo,
   dark-amber-on-indigo). Light variants for the collapsed header. */
.sfp-interact:not(.is-expanded) .sfp-chat-state-yourturn,
.sfp-interact:not(.is-expanded) .sfp-chat-state-icon-yourturn { color: #fff !important; }
.sfp-interact:not(.is-expanded) .sfp-chat-state-text.is-leader-replied,
.sfp-interact:not(.is-expanded) .sfp-chat-state-icon.is-leader-replied { color: #d3f9ee !important; }
.sfp-interact:not(.is-expanded) .sfp-chat-state-waiting,
.sfp-interact:not(.is-expanded) .sfp-chat-state-icon-waiting { color: #ffd66b !important; }

/* Answer card — the response lives inside the panel, above the
   discussion. White on the #f8f9fa chat wall so it reads as focal. */
.sfp-answer-card {
  background: #fff;
  border: 1px solid #e4e4e4;
  border-radius: 10px;
  padding: 0.875rem 1rem 1rem;
  margin-bottom: 10px;
}
.sfp-answer-form { margin: 0; }
.sfp-answer-instruction {
  display: block;
  font-size: 0.75rem;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.03em;
  color: #6c757d;
  margin: 0 0 0.625rem;
}
.sfp-answer-fieldset {
  border: 0;
  margin: 0;
  padding: 0;
  min-width: 0;          /* override the UA fieldset min-width */
}
.sfp-answer-feedback {
  font-size: 0.8rem;
  font-weight: 600;
  min-width: 0;
}
.sfp-answer-feedback.is-ok { color: var(--sfp-answered); }
.sfp-answer-feedback.is-error { color: #dc3545; }
.sfp-retry-btn {
  margin-left: 6px;
  padding: 0;
  background: none;
  border: 0;
  color: var(--sfp-brand);
  font-weight: 600;
  font-size: 0.8rem;
  text-decoration: underline;
  cursor: pointer;
}
/* Checkbox parity with the radio option styling (v7.72.10). */
.sfp-choice input[type="checkbox"] {
  width: 20px; height: 20px;
  flex-shrink: 0;
  accent-color: var(--sfp-indigo);
  cursor: pointer;
}

/* ─────────────────────────────────────────────────────────────────────────
 * v7.72.13 — Exercise library list: card-visual thumbnail, unified
 * search + tag-filter toolbar, and mobile-friendly action buttons.
 * ───────────────────────────────────────────────────────────────────────── */

/* Card-visual thumbnail. Reuses .sfp-lobby-card-visual (so the gradient,
 * dark-icon, and has-image rules all apply) at a small fixed size. These
 * declarations come after the lobby base + its mobile @media overrides in
 * source order, so equal-specificity properties (min-height, flex, icon
 * font-size) resolve in our favour without !important. */
.sf-lib-thumb {
  flex: 0 0 64px;
  width: 64px;
  height: 64px;
  min-height: 0;
  border-radius: 8px;
}
.sf-lib-thumb .bi {
  font-size: 1.6rem;
}

/* Unified search + tag-filter toolbar. The search input carries a leading
 * magnifier icon (absolutely positioned), so it needs left padding. */
.library-search-wrap {
  position: relative;
}
.library-search-icon {
  position: absolute;
  top: 50%;
  left: 0.625rem;
  transform: translateY(-50%);
  color: var(--bs-secondary-color);
  pointer-events: none;
  font-size: 0.9rem;
}
.library-search-input {
  padding-left: 2rem;
}

/* Row action buttons (duplicate / delete) — a real touch target on mobile.
 * Replaces the old `py-0 px-2` which collapsed the button height so the
 * icons looked cramped and overflowed on narrow screens. */
.sf-lib-action-btn {
  min-width: 38px;
  min-height: 38px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  padding: 0.25rem 0.5rem;
  line-height: 1;
}

/* ─────────────────────────────────────────────────────────────────────────
 * v7.72.15 — Exercise editor clarity & collapse (items 6a / 6b / 6c).
 * ───────────────────────────────────────────────────────────────────────── */

/* 6a / v7.72.22 — element-type accents. A coloured border + matching type icon
 * and label so different element types are distinguishable at a glance in a
 * long list. Colours use the Bootstrap base palette (fixed across light/dark).
 * v7.72.22 changes (item 4): the accent now wraps the WHOLE card (was a left
 * edge only); Text uses a real colour (orange) instead of grey, which read as
 * "disabled"; and the type label in the header takes the accent colour next to
 * its icon, so the top of each card is clearly colour-coded. */
.element-card { border: 2px solid var(--bs-border-color); }
.element-card.sf-el-text     { border-color: var(--bs-orange); }
.element-card.sf-el-text     .element-type-icon,
.element-card.sf-el-text     .element-type-label { color: var(--bs-orange); }
.element-card.sf-el-interact { border-color: var(--bs-indigo); }
.element-card.sf-el-interact .element-type-icon,
.element-card.sf-el-interact .element-type-label { color: var(--bs-indigo); }
.element-card.sf-el-image    { border-color: var(--bs-teal); }
.element-card.sf-el-image    .element-type-icon,
.element-card.sf-el-image    .element-type-label { color: var(--bs-teal); }
.element-card.sf-el-link     { border-color: var(--bs-pink); }
.element-card.sf-el-link     .element-type-icon,
.element-card.sf-el-link     .element-type-label { color: var(--bs-pink); }
.element-card.sf-el-table    { border-color: var(--bs-green); }
.element-card.sf-el-table    .element-type-icon,
.element-card.sf-el-table    .element-type-label { color: var(--bs-green); }
.element-card.sf-el-chart    { border-color: var(--bs-primary); }
.element-card.sf-el-chart    .element-type-icon,
.element-card.sf-el-chart    .element-type-label { color: var(--bs-primary); }

/* v12.0.69 (item 4a) — image-audit banner deep-link: clicking a flagged-image
   entry in the missing-image banner flashes a red ring around its element card so
   the leader can spot the element to fix. Red = the "needs fixing" semantics. */
.element-card.is-jump-highlight { animation: sf-jump-flash 2.2s ease-out; }
@keyframes sf-jump-flash {
  0%, 12% { box-shadow: 0 0 0 3px rgba(220, 53, 69, 0.55); background-color: rgba(220, 53, 69, 0.07); }
  100%    { box-shadow: 0 0 0 0 rgba(220, 53, 69, 0);     background-color: transparent; }
}
.flagged-jump { font-weight: 600; text-decoration: underline; cursor: pointer; }

/* v7.72.35 — item 2: make the per-type accent less subtle and give each card a
 * clearly-coloured top. A thicker left bar reads as the category colour, and the
 * header row gets a tinted band in the type colour (color-mix, with a neutral
 * fallback for any browser without color-mix). The icon + label colours above
 * stay as the third reinforcing cue. */
.element-card { border-left-width: 5px; }
.element-card .element-card-head {
  margin: 0 0 0.5rem;
  padding: 0.35rem 0.55rem;
  border-radius: 0.375rem;
  background: var(--bs-tertiary-bg); /* fallback when color-mix is unsupported */
}
.element-card.sf-el-text     .element-card-head { background: color-mix(in srgb, var(--bs-orange)  16%, var(--bs-body-bg)); }
.element-card.sf-el-interact .element-card-head { background: color-mix(in srgb, var(--bs-indigo) 14%, var(--bs-body-bg)); }
.element-card.sf-el-image    .element-card-head { background: color-mix(in srgb, var(--bs-teal)    16%, var(--bs-body-bg)); }
.element-card.sf-el-link     .element-card-head { background: color-mix(in srgb, var(--bs-pink)   14%, var(--bs-body-bg)); }
.element-card.sf-el-table    .element-card-head { background: color-mix(in srgb, var(--bs-green)  16%, var(--bs-body-bg)); }
.element-card.sf-el-chart    .element-card-head { background: color-mix(in srgb, var(--bs-primary) 14%, var(--bs-body-bg)); }
/* v16.24.0 — Calculation: cyan, darkened for white text (the owner's screenshot: the
   v16.23.0 button had NO rule and rendered white-on-white). */
.element-card.sf-el-calc     .element-card-head { background: color-mix(in srgb, var(--bs-cyan) 16%, var(--bs-body-bg)); }

/* CR.5 / item 1b — the "Add an element" buttons are FILLED in each type's accent
   colour (same palette as the element-card accents above), white text. The two
   lightest accents (orange, teal) are darkened a touch so white text stays
   legible; hover darkens slightly. */
.btn-add-el { border: 1px solid transparent; color: #fff; }
.btn-add-el-text     { background: #e8590c; background: color-mix(in srgb, var(--bs-orange) 92%, #000); }
.btn-add-el-interact { background: var(--bs-indigo); }
.btn-add-el-image    { background: #0f9c8a; background: color-mix(in srgb, var(--bs-teal) 84%, #000); }
.btn-add-el-link     { background: var(--bs-pink); }
.btn-add-el-table    { background: var(--bs-green); }
.btn-add-el-chart    { background: var(--bs-primary); }
.btn-add-el-calc     { background: #087d95; background: color-mix(in srgb, var(--bs-cyan) 62%, #000); } /* ≥4.5:1 with white text (computed 4.8) */
.btn-add-el:hover, .btn-add-el:focus-visible { color: #fff; filter: brightness(0.92); }

/* v12.0.55 — indigo button for leader-side interactive-element controls (the
 * per-element chat/responses access button in the visible-elements list),
 * matching "questions are indigo". Built on Bootstrap's --bs-btn-* vars so it
 * composes with `.btn .btn-sm`. --bs-indigo is #6610f2; hover/active step down
 * to #520dc2 ($indigo-600). */
.btn-sfp-indigo {
  --bs-btn-color: #fff;
  --bs-btn-bg: var(--bs-indigo);
  --bs-btn-border-color: var(--bs-indigo);
  --bs-btn-hover-color: #fff;
  --bs-btn-hover-bg: #520dc2;
  --bs-btn-hover-border-color: #520dc2;
  --bs-btn-focus-shadow-rgb: 102, 16, 242;
  --bs-btn-active-color: #fff;
  --bs-btn-active-bg: #520dc2;
  --bs-btn-active-border-color: #520dc2;
  --bs-btn-disabled-color: #fff;
  --bs-btn-disabled-bg: var(--bs-indigo);
  --bs-btn-disabled-border-color: var(--bs-indigo);
}

/* v12.0.192 (screenshot item 2d) — outline variant of the indigo access button,
 * for the live-elements list buttons (read as outlined controls, not filled
 * blocks). A locked/mixed element gets the .is-locked modifier: a lighter, muted
 * indigo so "open, waiting" elements stand out over "closed" ones. */
.btn-outline-sfp-indigo {
  --bs-btn-color: var(--bs-indigo);
  --bs-btn-bg: transparent;
  --bs-btn-border-color: var(--bs-indigo);
  --bs-btn-hover-color: #fff;
  --bs-btn-hover-bg: var(--bs-indigo);
  --bs-btn-hover-border-color: var(--bs-indigo);
  --bs-btn-focus-shadow-rgb: 102, 16, 242;
  --bs-btn-active-color: #fff;
  --bs-btn-active-bg: #520dc2;
  --bs-btn-active-border-color: #520dc2;
  --bs-btn-disabled-color: var(--bs-indigo);
  --bs-btn-disabled-bg: transparent;
  --bs-btn-disabled-border-color: var(--bs-indigo);
}
.reveal-row .reveal-thread-cluster.is-locked {
  --bs-btn-color: rgba(102, 16, 242, 0.50);
  --bs-btn-border-color: rgba(102, 16, 242, 0.30);
}
/* v12.0.205 (item A) — a MIXED element still has open threads, so its button is
   NOT muted like a fully-closed one; a faint amber border + an amber outline
   padlock signal that some threads are locked without saying "all locked". */
.reveal-row .reveal-thread-cluster.is-mixed {
  --bs-btn-border-color: rgba(224, 168, 0, 0.55);
  --bs-btn-hover-border-color: rgba(224, 168, 0, 0.75); /* v12.0.206 AUD.9 — don't revert to indigo on hover */
}
.reveal-thread-lock-mixed { color: #d98a00; }

/* v7.72.35 — item 1: the "Answer options" collapsible header now reads as a
 * clear, clickable section bar (it was muted small text that looked like a
 * plain label). Full-width, bordered, tinted, with a chevron + a count badge. */
.sf-options-toggle {
  background: var(--bs-tertiary-bg);
  border: 1px solid var(--bs-border-color);
  border-radius: 0.375rem;
  padding: 0.4rem 0.6rem;
  color: var(--bs-body-color);
  text-align: left;
}
.sf-options-toggle:hover { background: var(--bs-secondary-bg); }

/* 6b / 6c — collapse chevron. A standard Bootstrap chevron icon
 * (bi-chevron-down) that points down when expanded and rotates to point
 * right when collapsed. Used by the role-card headers (6b) and the
 * options-block toggles (6c). The state lives in editor state and survives
 * the editor's re-renders, so we drive the rotation off a `.collapsed`
 * class rather than Bootstrap's native collapse JS. */
.sf-collapse-chev {
  display: inline-block;
  transition: transform 0.15s ease-in-out;
  font-size: 0.85rem;
  color: var(--bs-secondary-color);
}
.sf-collapse-toggle.collapsed .sf-collapse-chev {
  transform: rotate(-90deg);
}
/* v7.72.22 — make collapsible section headers (e.g. "Answer options") read
 * clearly as interactive toggles rather than plain titles: a hover background,
 * a comfortable hit area, and rounded corners signal "this is clickable". */
.sf-collapse-toggle {
  cursor: pointer;
  border-radius: 0.375rem;
  padding: 0.25rem 0.4rem !important;
  transition: background-color 0.12s ease-in-out;
}
.sf-collapse-toggle:hover,
.sf-collapse-toggle:focus-visible {
  background-color: var(--bs-tertiary-bg);
}
.sf-collapse-toggle:hover .sf-collapse-chev {
  color: var(--bs-primary);
}

/* 6c — option rows: semi-bold label text so each option reads as a distinct,
 * scannable row inside the (now collapsible) options block. */
.sf-options-body [data-opt-field="label"] {
  font-weight: 500;
}
/* ─────────────────────────────────────────────────────────────────────────
 * v7.72.20 — Exercise editor: reachable Save on long/small screens.
 * ───────────────────────────────────────────────────────────────────────── */

/* Sticky action bar. On a long form (or a small screen) the Save button used
 * to be stranded at the very bottom, past the editor and a tall preview, and
 * on iOS could sit under the browser chrome. Pinning the action row to the
 * bottom of the viewport keeps Save (+ Cancel) reachable while editing.
 * Degrades to a normal in-flow bar where position:sticky isn't honoured. The
 * safe-area padding clears the iOS home indicator.
 *
 * v7.72.21 — the journey design mode's Edit/Cancel/Save toolbar reuses the
 * same sticky behaviour via .sf-sticky-actions (it was moved from the top of
 * the pane to the bottom so Save is reachable after scrolling a long list). */
.sf-editor-actions,
.sf-sticky-actions {
  position: sticky;
  bottom: 0;
  z-index: 5;
  margin-top: 1.5rem;
  padding: 0.75rem 0;
  padding-bottom: calc(0.75rem + env(safe-area-inset-bottom, 0px));
  background: var(--bs-body-bg);
  border-top: 1px solid var(--bs-border-color);
}

/* v7.72.21 — bottom clearance under the editor's sticky action bar, so the
 * last content (the danger zone) can never sit hidden behind the bar at full
 * scroll, even under iOS Safari's sticky/viewport quirks. Sized a little above
 * the bar's own height; includes the safe-area inset. */
.sf-editor-bottom-gap {
  height: calc(4rem + env(safe-area-inset-bottom, 0px));
}

/* Preview pane height cap + internal scroll only apply on lg+ viewports,
 * where the preview sits sticky BESIDE the form. On narrow viewports the
 * columns stack and a capped, internally-scrolling preview between the editor
 * and the Save button became a scroll trap; here we let it flow naturally so
 * normal page scrolling reaches the actions below it. */
@media (min-width: 992px) {
  .sf-editor-preview-body {
    max-height: min(60vh, calc(100vh - 160px));
    overflow-y: auto;
  }
}

/* v8.3.4 (#1) — universal in-button spinner for JS/AJAX-driven Save / submit /
   send buttons. The handler sets aria-busy="true" while the request is in
   flight and removes it when done; this prepends a spinner that inherits the
   button's text colour (so it works on filled AND outline buttons) and blocks
   further clicks. Non-AJAX form submits already get a spinner via
   form-feedback.js, so this complements rather than duplicates it. */
.btn[aria-busy="true"] { pointer-events: none; }
.btn[aria-busy="true"]::before {
  content: "";
  display: inline-block;
  width: 1em;
  height: 1em;
  margin-right: 0.45em;
  vertical-align: -0.125em;
  border: 0.18em solid currentColor;
  border-right-color: transparent;
  border-radius: 50%;
  animation: sf-btn-spin 0.6s linear infinite;
}
@keyframes sf-btn-spin { to { transform: rotate(360deg); } }

/* v8.3.4 (#1) — icon-only send buttons (chat composer + leader responder) swap
   their arrow glyph for the spinner when busy, so it sits centred rather than
   crowding the icon. */
/* v8.3.4 (#1) — icon-only send buttons (chat composer + leader responder) swap
   their arrow glyph for the spinner when busy, so it sits centred rather than
   crowding the icon. These aren't Bootstrap .btn elements, so the spinner
   geometry is declared here directly (white, to read on the blue/grey button)
   rather than inherited from the .btn[aria-busy] rule. */
.sfp-chat-send[aria-busy="true"], .responses-send-btn[aria-busy="true"] { position: relative; pointer-events: none; }
.sfp-chat-send[aria-busy="true"] > svg, .responses-send-btn[aria-busy="true"] > svg { visibility: hidden; }
.sfp-chat-send[aria-busy="true"]::before, .responses-send-btn[aria-busy="true"]::before {
  content: "";
  position: absolute; top: calc(50% - 0.5em); left: calc(50% - 0.5em); margin: 0;
  width: 1em; height: 1em;
  border: 0.18em solid #fff;
  border-right-color: transparent;
  border-radius: 50%;
  animation: sf-btn-spin 0.6s linear infinite;
}

/* ── v12.0.59 — Exercise-tab image audit panel + detail overlay ── */
.min-w-0 { min-width: 0; }

/* v14.51.0 — participant learning list (feedback batch item 1). */
.sfp-new-pill {                       /* subtle yellow "unread" marker */
  display: inline-block;
  background: #fff3cd;
  color: #664d03;
  border: 1px solid #ffe69c;
  border-radius: 999px;
  font-size: 0.62rem;
  font-weight: 600;
  line-height: 1.4;
  padding: 0 0.45rem;
  text-transform: lowercase;
  vertical-align: middle;
}
.sfp-tag-pill {                       /* v14.55.0 (participant report 3) —
  quieter: white ground, light-grey outline + text, instead of the filled
  grey chip that competed with the card content. Still readable (#6c757d on
  white is 4.7:1), just less present. The .is-active state keeps its filled
  look so the selected tag still stands out. */
  display: inline-block;
  background: #fff;
  border: 1px solid var(--bs-border-color, #dee2e6);
  color: var(--bs-secondary-color, #6c757d);
  border-radius: 999px;
  font-size: 0.62rem;
  line-height: 1.5;
  padding: 0 0.5rem;
  margin-right: 0.25rem;
  cursor: pointer;
  user-select: none;
}
.sfp-tag-pill:hover, .sfp-tag-pill:focus-visible { background: var(--bs-secondary-border-subtle, #dee2e6); color: var(--bs-body-color, #212529); }
.sfp-tag-pill.is-active { background: #664d03; color: #fff3cd; }
.sf-kebab {                            /* v14.52.0 (A1) — native-details kebab menu */
  position: relative;
  display: inline-block;
}
.sf-kebab > summary { list-style: none; cursor: pointer; display: inline-block; }
.sf-kebab > summary::-webkit-details-marker { display: none; }
/* v14.54.0 (admin report 1) — the *modern* marker too: list-style:none +
 * the legacy -webkit pseudo left Safari drawing its default disclosure
 * chevron via ::marker on the […] buttons. Both channels now suppressed. */
.sf-kebab > summary::marker { content: none; display: none; }
/* v14.56.0 (leader report 3a) — belt AND braces: some engines keep drawing
   the disclosure triangle unless the summary stops being a list-item
   entirely. Applied to every .sf-kebab summary regardless of nesting depth
   (the assign-leader summary is styled as a link-button and was still
   marked). */
.sf-kebab summary { list-style: none; display: inline-block; }
.sf-kebab summary::marker { content: none; display: none; }
.sf-kebab summary::-webkit-details-marker { display: none; }
/* v14.54.0 (admin report 1) — the OPEN kebab lifts above its neighbours.
 * All menus share z-index 1050 INSIDE their own stacking scope, so with two
 * open at once the later one in the DOM painted over the earlier ("menus
 * partially sit behind elements"). Single-open is enforced in admin.js;
 * this is the belt-and-braces for the one frame where two could coexist. */
.sf-kebab[open] { z-index: 1060; }
/* v14.57.1 — no-JS fallback: a native details that admin.js has not
   enhanced (script blocked / failed) still shows its menu when opened. */
details.sf-kebab[open] > .sf-kebab-menu { display: block; }
.sf-kebab-home { position: relative; }
/* v14.56.0 (leader report 3d) — a project's journeys read as one blur when
   stacked; each row now carries its own hairline + breathing room, and on
   phones the row wraps instead of shoving the kebab off-canvas. */
.sf-projects-journey { padding: .3rem 0; border-top: 1px solid var(--bs-border-color-translucent, rgba(0,0,0,.08)); flex-wrap: wrap; }
.sf-projects-journey:first-of-type { border-top: 0; padding-top: 0; }
/* v14.56.0 (leader report 3b) — buttons keep their shape on small screens:
   no flex-stretch, no label wrap. */
.sf-projects-journey .btn, .sf-kebab summary.btn { flex-shrink: 0; white-space: nowrap; align-self: center; }
.sf-kebab[open] > summary { background: var(--bs-secondary-bg, #e9ecef); }
.sf-kebab-menu {
  /* v14.57.1 — the menu is JS-portaled to <body> with position:fixed when
     open (admin.js): the third round of the projects-page fight ended by
     abandoning <details> menus entirely. In its closed home the menu is
     simply hidden; every position/z rule now comes from the portal. */
  display: none;
  position: absolute;                  /* harmless pre-enhancement fallback */
  right: 0;
  z-index: 1050;
  min-width: 11rem;
  margin-top: 0.15rem;
  padding: 0.25rem;
  background: var(--bs-body-bg, #fff);
  border: 1px solid var(--bs-border-color, #dee2e6);
  border-radius: 0.5rem;
  box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15);
}
.sf-kebab-menu-wide { min-width: 21rem; }
.sf-kebab-menu .btn:hover { background: var(--bs-secondary-bg, #e9ecef); }

.sfp-jump-to-form {                   /* v14.52.0 (P3) — pinned thread-tail affordance */
  display: block;
  width: 100%;
  border: 1px dashed var(--bs-warning-border-subtle, #ffe69c);
  background: var(--bs-warning-bg-subtle, #fff3cd);
  color: #664d03;
  border-radius: 0.5rem;
  font-size: 0.8rem;
  padding: 0.35rem 0.6rem;
  margin-top: 0.35rem;
  cursor: pointer;
}
.sfp-jump-to-form:hover { filter: brightness(0.97); }

.sfp-tag-filterbar {
  font-size: 0.8rem;
  color: var(--bs-secondary-color, #6c757d);
}
.sfp-imgaudit-thumb {
  width: 56px; height: 56px; flex: 0 0 auto;
  border-radius: 8px; overflow: hidden;
  background: #f1f3f5; border: 1px solid #e6e8ec;
  display: flex; align-items: center; justify-content: center;
}
.sfp-imgaudit-thumb img { width: 100%; height: 100%; object-fit: cover; display: block; }
.sfp-imgaudit-thumb--empty { color: #adb5bd; font-size: 20px; }
.sfp-imgaudit-overlay {
  position: fixed; inset: 0; z-index: 2000;
  background: rgba(15, 18, 20, 0.82);
  display: flex; align-items: center; justify-content: center;
  padding: 24px;
}
.sfp-imgaudit-dialog {
  display: flex; flex-direction: column;
  width: 100%; max-width: 900px; max-height: 90vh;
  background: #fff; border-radius: 12px; overflow: hidden;
  box-shadow: 0 12px 40px rgba(0, 0, 0, 0.4);
}
.sfp-imgaudit-dialog-head {
  display: flex; align-items: center; gap: 10px;
  padding: 10px 14px; background: #212529; color: #fff;
}
.sfp-imgaudit-imgwrap {
  flex: 1 1 auto; min-height: 0;
  display: flex; align-items: center; justify-content: center;
  background: #1a1d20; padding: 12px;
}
.sfp-imgaudit-imgwrap img { max-width: 100%; max-height: 72vh; object-fit: contain; display: block; }
.sfp-imgaudit-imgwrap:fullscreen { background: #000; }
.sfp-imgaudit-imgwrap:fullscreen img { max-height: 100vh; }
.sfp-imgaudit-meta {
  padding: 12px 14px; background: #fff; border-top: 1px solid #e6e8ec;
  display: flex; flex-direction: column; gap: 3px;
}

/* ── v12.0.61 — image audit triage layout ── */
.sfp-imgaudit-thumb--btn { padding: 0; cursor: zoom-in; }
.sfp-imgaudit-thumb--btn:hover { border-color: #adb5bd; }
.sfp-imgaudit-thumb--btn:focus-visible { outline: 2px solid #0d6efd; outline-offset: 2px; }
.sfp-imgaudit-ready > summary { cursor: pointer; user-select: none; }
.sfp-imgaudit-ready > summary:hover { background: #f8f9fa; }

/* ─────────────────────────────────────────────────────────────────────
   v12.0.188 — "what needs you" popover (#8). A calm, dismissible list of
   which challenges have groups waiting on the leader, anchored under the
   action-aware cockpit badge (positioned via JS, fixed + viewport-clamped).
   Amber accents match the responses modal's "Needs you" section.
   ───────────────────────────────────────────────────────────────────── */
.cockpit-needs-popover {
  position: fixed;
  z-index: 1040;
  width: 288px;
  max-width: calc(100vw - 16px);
  background: #fff;
  border: 1px solid #e3d6a8;
  border-radius: 10px;
  box-shadow: 0 8px 28px rgba(0, 0, 0, 0.16);
  overflow: hidden;
  font-size: 0.9rem;
}
.cockpit-needs-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 8px;
  padding: 9px 12px;
  background: #fff3cd;
  color: #7a4f01;
  font-weight: 700;
  font-size: 0.8rem;
  letter-spacing: 0.02em;
}
.cockpit-needs-close {
  border: 0;
  background: transparent;
  color: #7a4f01;
  font-size: 1.25rem;
  line-height: 1;
  padding: 0 2px;
  cursor: pointer;
  opacity: 0.8;
}
.cockpit-needs-close:hover { opacity: 1; }
.cockpit-needs-close:focus-visible { outline: 2px solid #0d6efd; outline-offset: 1px; }
.cockpit-needs-list { list-style: none; margin: 0; padding: 4px; max-height: 320px; overflow-y: auto; }
.cockpit-needs-item {
  display: flex;
  align-items: center;
  gap: 8px;
  width: 100%;
  padding: 8px 10px;
  border: 0;
  border-radius: 7px;
  background: transparent;
  text-align: left;
  cursor: pointer;
}
.cockpit-needs-item:hover { background: #f6f2e6; }
.cockpit-needs-item:focus-visible { outline: 2px solid #0d6efd; outline-offset: -1px; }
.cockpit-needs-item-name { flex: 1 1 auto; color: #212529; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.cockpit-needs-item-count {
  flex: 0 0 auto;
  min-width: 22px;
  height: 22px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  padding: 0 6px;
  border-radius: 11px;
  background: #ffe083;
  color: #664d03;
  font-weight: 700;
  font-variant-numeric: tabular-nums;
  font-size: 0.78rem;
}
.cockpit-needs-foot { padding: 7px 12px; border-top: 1px solid #f0f0f0; color: #868e96; font-size: 0.74rem; }
.cockpit-needs-empty { padding: 16px 12px; color: #5c636a; text-align: center; }

/* v12.0.201 (screenshot round 2, item 4a) — dismiss affordances on the needs-you
   popover: a per-row × (mark that challenge seen) and a footer "Mark all seen". */
.cockpit-needs-item-row { display: flex; align-items: stretch; }
.cockpit-needs-item-row .cockpit-needs-item { flex: 1 1 auto; }
.cockpit-needs-dismiss {
  flex: 0 0 auto; border: none; background: transparent; color: #adb5bd;
  font-size: 1.1rem; line-height: 1; padding: 0 8px; cursor: pointer; border-radius: 6px;
}
.cockpit-needs-dismiss:hover { color: #495057; background: #efeae0; }
.cockpit-needs-dismiss:focus-visible { outline: 2px solid #0d6efd; outline-offset: -1px; }
.cockpit-needs-foot { display: flex; align-items: center; justify-content: space-between; gap: 8px; }
.cockpit-needs-dismiss-all {
  flex: 0 0 auto; border: 1px solid #e3d6a8; background: #fff; color: #664d03;
  font-size: 0.72rem; font-weight: 600; padding: 3px 8px; border-radius: 6px;
  cursor: pointer; white-space: nowrap;
}
.cockpit-needs-dismiss-all:hover { background: #fff3cd; }

/* Brief highlight on the challenge card a popover row jumps to. */
.cockpit-needs-flash { animation: cockpitNeedsFlash 1.6s ease-out; }
@keyframes cockpitNeedsFlash {
  0%   { box-shadow: 0 0 0 3px rgba(224, 168, 0, 0.55); }
  100% { box-shadow: 0 0 0 3px rgba(224, 168, 0, 0); }
}

/* ═══ Mission Control (v12.0.209 — design/mission-control.md) ═══
   The cockpit's triage bar + feed. Colour is never the only signal (icons +
   text everywhere); amber = "waiting on you" matches the responses modal's
   needs-you section; the calm all-clear is green text, not an empty frame. */
.mc-wrap { margin-bottom: 1rem; }
.mc-bar {
  display: flex; align-items: center; gap: 8px;
  width: 100%; text-align: left;
  padding: 9px 12px;
  border-radius: 10px;
  font-weight: 600; font-size: 0.92rem;
  cursor: pointer;
  /* v15.14.0 (field report 4) — the states were INVERTED vs the ruling
     (and vs the v15.11.0 collapse convention everywhere else): quiet when
     closed, strong when open. Now: COLLAPSED (aria-expanded="false") =
     LIGHT indigo tint; EXPANDED = full indigo with white text. The bar's
     aria-expanded is toggled by the panel handler, so CSS alone tracks
     the state. (--sfp-* vars live under body.sf-p only — on this leader
     page the bare var() computed to TRANSPARENT here, which is why the
     v12.0.341 concrete fallbacks exist and stay.) */
  background: var(--sfp-indigo-light, rgba(133,64,245,0.10));
  border: 1px solid rgba(133, 64, 245, 0.35);
  color: var(--sfp-indigo-dark, #6610f2);
}
.mc-bar[aria-expanded="true"] {
  background: var(--sfp-indigo, #8540f5);
  border-color: var(--sfp-indigo, #8540f5);
  color: #fff;
}
.mc-bar:hover { background: rgba(133, 64, 245, 0.16); }
.mc-bar[aria-expanded="true"]:hover { background: var(--sfp-indigo-dark, #6610f2); }
.mc-bar:focus-visible { outline: 2px solid var(--sfp-indigo, #8540f5); outline-offset: 1px; }
/* icon + chevron follow the text colour so they read on either state */
.mc-bar-icon { color: inherit; }
.mc-bar:not([aria-expanded="true"]) .mc-bar-icon { color: var(--sfp-indigo, #8540f5); }
/* v14.9.1 — summary-bar buttons that exist in one cockpit mode only.
   cockpit.js setMode() stamps body[data-cockpit-mode]. Before the first
   stamp, .sf-only-in-design stays hidden (safe default). */
body[data-cockpit-mode="design"] .sf-hide-in-design { display: none !important; }
body:not([data-cockpit-mode="design"]) .sf-only-in-design { display: none !important; }

.mc-pill { font-size: 0.76rem; font-weight: 700; padding: 2px 8px; border-radius: 999px; white-space: nowrap; }
.mc-pill-needs { background: #fff3cd; color: #664d03; border: 1px solid #ffe69c; }
.mc-pill-new { background: var(--bs-secondary-bg, #e9ecef); color: #495057; }
.mc-allclear { font-size: 0.8rem; font-weight: 600; color: #198754; display: inline-flex; align-items: center; gap: 4px; }
/* v12.0.269 (FR3.4) — needs are clear but something is ending with coverage gaps. */
.mc-allclear.is-gaps { color: #b45309; }
/* v12.0.377 (leader feedback 2), FLIPPED at v15.15.0 (field report 1):
   the white-text rule follows the FULL-INDIGO surface, and v15.14.0 moved
   that surface from collapsed to EXPANDED. On the light collapsed bar the
   all-clear line, summary counters, and stale marker keep their natural
   colours (white there was the bug this fixes); on the expanded indigo
   they go white, same readability reasoning as v377. Chips with their own
   light background (.mc-pill-*, .mc-waiting-pill, .mc-sum-alert,
   .mc-sum-away) keep their colours on BOTH states — they carry their own
   surface. */
.mc-bar[aria-expanded="true"] .mc-allclear,
.mc-bar[aria-expanded="true"] .mc-allclear.is-gaps,
.mc-bar[aria-expanded="true"] .mc-sum,
.mc-bar[aria-expanded="true"] .mc-stale { color: #fff; }
/* v12.0.270 (FR3.9) — the Coach tab's count pill + the element-list grade badge. */
.mc-lens-badge { display: inline-block; min-width: 1.2em; margin-left: 6px; padding: 0 5px; border-radius: 999px; background: #b45309; color: #fff; font-size: 0.7rem; font-weight: 700; line-height: 1.5; text-align: center; }
.reveal-thread-ungraded { margin-left: 4px; }
/* v12.0.271 (FR3.11) — the MC XP lens setting card; (FR3.13c) the progress
   board matches the level block's width. */
.mc-xp-setting { padding: 10px 12px; }
.mc-xp-head { margin-bottom: 2px; }
.mc-xp-head .bi { margin-right: 6px; }
.mc-xp-desc { font-size: 0.8rem; color: var(--bs-secondary-color); margin-bottom: 8px; }
/* v12.0.326 (feedback P4) — the board matches the level card EXACTLY:
 * the card is max-width 480px, so the board fills its wrapper up to the
 * same 480 (overriding the base inline-block 340 cap + the old 420). */
.sfp-progress-board { max-width: 480px; }
.sfp-progress-board .sfp-lobby-board { display: block; width: 100%; max-width: none; box-sizing: border-box; }
/* v12.0.273 (FR4.3) — the chart data-table toggle + the multi-metric table. */
.sfp-chart-toggle { display: flex; gap: 4px; justify-content: flex-end; margin-bottom: 6px; }
.sfp-chart-toggle-btn { font-size: 0.72rem; padding: 1px 8px; border: 1px solid var(--bs-border-color); background: var(--bs-body-bg); border-radius: 999px; color: var(--bs-secondary-color); }
.sfp-chart-toggle-btn.is-active { background: var(--bs-primary); border-color: var(--bs-primary); color: #fff; }
.sfp-chart-tablewrap { overflow-x: auto; }
/* v12.0.275 (FR3.13a/b) — the progress page's expandable breakdowns. */
.sfp-break-toggle { border: 0; background: transparent; color: var(--bs-secondary-color); padding: 0 2px; margin-left: 6px; }
.sfp-break-items { margin: 2px 0 8px 26px; }
.sfp-break-list { list-style: none; margin: 0; padding: 0; }
.sfp-break-item { display: flex; gap: 8px; align-items: baseline; font-size: 0.82rem; padding: 2px 0; }
.sfp-break-xp { font-weight: 700; color: #198754; min-width: 3.2em; }
.sfp-break-xp.is-neg { color: #dc3545; }
.sfp-break-detail { color: var(--bs-secondary-color); }
.sfp-break-empty { color: var(--bs-secondary-color); font-size: 0.82rem; }
.sfp-break-more { border: 1px solid var(--bs-border-color); background: var(--bs-body-bg); border-radius: 6px; font-size: 0.78rem; padding: 2px 10px; margin-top: 4px; }

/* v12.0.276 (FR3.14) — the journey header: title flexes, switcher never shifts. */
.jn-title-wrap { flex: 1 1 auto; min-width: 0; }
.jn-title-wrap .h4.text-truncate { max-width: 100%; }

/* v12.0.276 (FR3.16) — the participant table bar reads like its sibling
   headers: no hard outline, calmer size, a real right-aligned chevron. */
.sfp-table-toggle-btn { border: 1px solid transparent; font-size: 0.95rem; }
.sfp-table-toggle-btn::after { margin-left: auto; font-size: 1.15em; line-height: 1; opacity: 0.7; }

/* v12.0.276 (FR3.15) — arrivals must be visible from LIVE: the amber door
   pill on the Mission Control bar. */
.mc-waiting-pill { display: inline-flex; align-items: center; gap: 4px; font-size: 0.78rem; font-weight: 700; color: #664d03; background: #fff3cd; border: 1px solid #ffe69c; border-radius: 999px; padding: 1px 9px; text-decoration: none; }
.mc-waiting-pill:hover { background: #ffe69c; color: #4d3a02; }

/* v12.0.278 (FR5.6) — one bottom composer slot for open questions: the answer
   field lives BELOW the thread and sticks to the viewport bottom while its
   panel is on screen (the leader-composer feel); once answered, the chat
   composer occupies the same slot. .is-revising brings the answer field back
   (prefilled) and hides the chat input. */
.sfp-answer-card.sfp-answer-below {
  /* v14.49.0 — same pin line as the composer (--sfp-composer-gap): this slot
     alternates with the chat composer, and now that the sticky genuinely pins
     (overflow: clip on .sfp-chat), bottom: 0 would sit under the poll pill
     exactly like the composer did. One variable, one line, every state. */
  position: sticky; bottom: var(--sfp-composer-gap, 8px); z-index: 2;
  background: var(--bs-body-bg, #fff);
  border-top: 1px solid #e9ecef;
  padding-top: 0.5rem;
  box-shadow: 0 -6px 12px -10px rgba(0, 0, 0, 0.25);
}
/* v14.49.0 — same pin line as the base rule: this override's `bottom: 0`
   was a SECOND inconsistency across the exact is-answered flip the b+c
   report describes. */
.sfp-chat.is-answered .sfp-chat-composer { position: sticky; bottom: var(--sfp-composer-gap, 8px); z-index: 2; background: var(--bs-body-bg, #fff); }
.sfp-chat.is-revising .sfp-answer-card { display: block !important; }
.sfp-chat.is-revising .sfp-chat-composer, .sfp-chat.is-revising .sfp-chat-revise { display: none !important; }
.sfp-chat-revise {
  display: inline-flex; align-items: center; gap: 5px;
  border: 1px dashed #6f42c1; color: #6f42c1; background: transparent;
  border-radius: 8px; font-size: 0.8rem; font-weight: 600;
  padding: 3px 10px; margin: 6px 0 4px;
}
.sfp-chat-revise:hover { background: #f3edfb; }
.sfp-chat-revise-cancel {
  border: 0; background: transparent; color: var(--bs-secondary-color);
  font-size: 0.78rem; text-decoration: underline; margin-top: 4px;
}

/* v12.0.279 (FR5.3, design/options-overflow.md) — the compact option list.
   >8 options: uniform rows in a capped scroll WINDOW over ALL options (never
   a subset); the show-all toggle releases the cap. Selection = the label;
   ⓘ is a sibling target expanding the full title + image in-place. */
.sfp-optlist {
  display: flex; flex-direction: column; gap: 4px;
  max-height: 15.5rem; overflow-y: auto;
  border: 1px solid #e9ecef; border-radius: 10px; padding: 6px;
  scrollbar-gutter: stable;
}
.sfp-optlist-toggle:checked ~ [data-sfp-choice-list].sfp-optlist { max-height: none; overflow: visible; }
.sfp-optlist-head { display: flex; align-items: center; gap: 6px; margin: 2px 2px 4px; font-size: 0.78rem; color: var(--bs-secondary-color); }
.sfp-optlist-showall { color: #6f42c1; font-weight: 600; cursor: pointer; text-decoration: underline; }
.sfp-optlist-showall-close { display: none; }
.sfp-optlist-toggle:checked ~ .sfp-optlist-head .sfp-optlist-showall-open { display: none; }
.sfp-optlist-toggle:checked ~ .sfp-optlist-head .sfp-optlist-showall-close { display: inline; }
.sfp-optlist-toggle:focus-visible ~ .sfp-optlist-head .sfp-optlist-showall { outline: 2px solid #6f42c1; outline-offset: 2px; border-radius: 4px; }

.sfp-optrow { display: flex; flex-wrap: wrap; align-items: center; border: 1px solid #e3e0f0; border-radius: 8px; background: #fff; }
.sfp-optrow.is-selected { border-color: #6f42c1; background: #f6f2fc; }
.sfp-optrow-main { display: flex; align-items: center; gap: 8px; flex: 1 1 auto; min-width: 0; padding: 8px 4px 8px 10px; cursor: pointer; margin: 0; }
.sfp-optrow-main input { flex: 0 0 auto; }
.sfp-optrow-thumb { width: 32px; height: 32px; object-fit: cover; border-radius: 6px; flex: 0 0 auto; }
.sfp-optrow-title { flex: 1 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: 0.9rem; font-weight: 500; }
.sfp-optrow-info { flex: 0 0 auto; padding: 8px 10px; color: #6c757d; cursor: pointer; margin: 0; border-left: 1px solid #f1f0f6; }
.sfp-optrow-info:hover { color: #6f42c1; }
/* v15.0.1 (report PL2) — on a phone the option row is a few pixels too
   narrow, so the (i) button wrapped to its own line. The row is a flex line
   whose title is `white-space: nowrap` with ellipsis; the pressure is all on
   the title. Two changes, narrow screens only:
     • the title may use TWO lines before ellipsing, so a long label is
       readable without opening the detail at all;
     • slightly smaller type and tighter (i) padding buy back the width that
       made it wrap.
   Desktop is untouched — the report says it is fine there, and one-line rows
   scan better when there is room for them. */
@media (max-width: 576px) {
  .sfp-optrow-title {
    white-space: normal;
    display: -webkit-box;
    -webkit-line-clamp: 2;
    -webkit-box-orient: vertical;
    overflow: hidden;
    font-size: 0.82rem;
    line-height: 1.25;
  }
  .sfp-optrow-info { padding: 6px 8px; }
}
.sfp-optrow-info-toggle:focus-visible ~ .sfp-optrow-info { outline: 2px solid #6f42c1; outline-offset: -2px; border-radius: 6px; }
.sfp-optrow-detail { display: none; flex-basis: 100%; padding: 0 10px 10px 10px; }
.sfp-optrow-info-toggle:checked ~ .sfp-optrow-detail { display: block; }
.sfp-optrow-detail-title { font-size: 0.88rem; line-height: 1.45; color: #343a40; white-space: normal; }
.sfp-optrow-detail-img { max-width: min(320px, 100%); border-radius: 8px; margin-top: 8px; display: block; }
/* leader-side metric chips inside a detail (participant details never carry them) */
.sfp-optrow-metrics { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 8px; }
.sfp-optrow-metric { font-size: 0.72rem; font-weight: 700; font-variant-numeric: tabular-nums; background: #eef2ff; color: #3730a3; border-radius: 999px; padding: 1px 8px; }
.mc-bar-chevron { margin-left: auto; color: inherit; transition: transform 0.15s ease; } /* v12.0.326 — follows the bar text colour (white collapsed, dark expanded) */
.mc-bar[aria-expanded="true"] .mc-bar-chevron { transform: rotate(180deg); }
@media (prefers-reduced-motion: reduce) { .mc-bar-chevron { transition: none; } }
.mc-wrap.is-open .mc-bar { border-radius: 10px 10px 0 0; }
.mc-panel {
  /* v15.15.0 (field report 2) — 2px, matching the live challenge cards
     (.live-now-card): the 1px outline read as an afterthought around an
     open panel. Same rule for the green/yellow details below. */
  border: 2px solid rgba(133, 64, 245, 0.25); border-top: 0;
  border-radius: 0 0 10px 10px;
  background: #fff;
  padding: 4px 6px 6px;
}
.mc-row {
  display: flex; align-items: center; gap: 10px;
  width: 100%; text-align: left;
  background: transparent; border: 0; border-radius: 8px;
  padding: 8px;
  cursor: pointer;
  font-size: 0.87rem;
}
.mc-row:hover { background: rgba(133, 64, 245, 0.06); }
.mc-row:focus-visible { outline: 2px solid var(--sfp-indigo); outline-offset: -2px; }
.mc-row:not(:last-child) { border-bottom: 1px solid #f1f3f5; border-radius: 0; }
.mc-row-flag { flex: 0 0 auto; display: inline-flex; align-items: center; gap: 5px; font-size: 0.78rem; font-weight: 700; }
.mc-row-flag.is-needs { color: #8a5a00; }
.mc-row-flag.is-new { color: #495057; font-weight: 600; }
.mc-row-wait {
  background: #fff3cd; border: 1px solid #ffe69c; color: #664d03;
  border-radius: 6px; padding: 0 6px;
  font-variant-numeric: tabular-nums;
}
.mc-row-path { flex: 1 1 auto; min-width: 0; display: flex; align-items: baseline; gap: 6px; }
.mc-row-ch { font-weight: 600; color: #212529; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-width: 45%; }
.mc-row-sep { color: #adb5bd; }
.mc-row-el { color: #495057; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.mc-row-unread { flex: 0 0 auto; font-size: 0.74rem; background: var(--bs-secondary-bg, #e9ecef); color: #495057; border-radius: 999px; padding: 1px 7px; }
.mc-empty { padding: 10px 8px; font-size: 0.85rem; color: #198754; display: flex; align-items: center; gap: 6px; }

/* ── Mission Control on /l/live (v12.0.210) — the cross-journey rows ──
   Same feed language as the cockpit, plus a journey eyebrow, an honest ⏸
   Paused chip (frozen label, never a broken countdown), the AN.1a urgency
   ring as the row's status disc, and the red staleness note. */
.mc-row-live { text-decoration: none; color: inherit; }
.mc-row-live:hover { text-decoration: none; }
.mc-row-main { flex: 1 1 auto; min-width: 0; display: flex; flex-direction: column; gap: 2px; }
.mc-journey { font-size: 0.72rem; font-weight: 600; color: #6c757d; display: inline-flex; align-items: center; gap: 6px; }
.mc-paused {
  display: inline-flex; align-items: center; gap: 3px;
  font-size: 0.7rem; font-weight: 700;
  color: #8a5a00; background: #fff3cd; border: 1px solid #ffe69c;
  border-radius: 999px; padding: 0 6px;
}
.mc-stale { font-size: 0.72rem; font-weight: 600; color: #b02a37; }
.mc-ring { flex: 0 0 auto; width: 22px; height: 22px; display: inline-flex; align-items: center; justify-content: center; }

/* ═ Mission Control v2 lenses (v12.0.215 — design/mission-control.md §v2) ═ */
.mc-lenses { display: flex; gap: 4px; padding: 4px 6px 8px; border-bottom: 1px solid #f1f3f5; margin-bottom: 4px; }
.mc-lens {
  border: 0; background: transparent; cursor: pointer;
  font-size: .78rem; font-weight: 600; color: #495057;
  padding: 3px 10px; border-radius: 999px;
}
.mc-lens:hover { background: #f1f3f5; }
.mc-lens:focus-visible { outline: 2px solid var(--sfp-indigo); outline-offset: 1px; }
.mc-lens.is-active { background: rgba(133, 64, 245, .10); color: var(--sfp-indigo-dark); }
.mc-lens-body { padding: 2px 0; }

/* Challenges lens */
.mc-ch-row { display: flex; align-items: center; gap: 10px; padding: 8px; font-size: .87rem; }
/* v12.0.233 (MC 2b) — the row became a button (jump to the challenge card):
   reset chrome, keep the flex layout, give it hover/focus affordance. */
button.mc-ch-row { width: 100%; text-align: left; background: none; border: 0; cursor: pointer; }
button.mc-ch-row:hover { background: #f8f6ff; }
button.mc-ch-row:focus-visible { outline: 2px solid var(--sfp-indigo); outline-offset: -2px; border-radius: 6px; }
.mc-ch-row:not(:last-child) { border-bottom: 1px solid #f1f3f5; }
.mc-ending {
  display: inline-flex; align-items: center; gap: 4px; margin-left: 6px;
  font-size: .72rem; font-weight: 700; padding: 0 7px; border-radius: 999px;
  background: #fff3cd; color: #8a5a00; border: 1px solid #ffe69c; white-space: nowrap;
}
.mc-ending.is-up { background: #f8d7da; color: #842029; border-color: #f1aeb5; }
@keyframes mc-target-pulse {
  0% { box-shadow: 0 0 0 0 rgba(133, 64, 245, .45); }
  100% { box-shadow: 0 0 0 14px rgba(133, 64, 245, 0); }
}
.mc-target-flash { animation: mc-target-pulse 1.1s ease-out 2; }
@media (prefers-reduced-motion: reduce) { .mc-target-flash { animation: none; outline: 2px solid var(--sfp-indigo); } }
.mc-cov { flex: 0 0 auto; font-size: .78rem; font-weight: 700; color: #495057; font-variant-numeric: tabular-nums; }
.mc-cov-all { display: inline-flex; align-items: center; gap: 4px; font-size: .78rem; font-weight: 600; color: #198754; }
.mc-missing { font-size: .76rem; color: #8a5a00; }

/* People lens */
.mc-person { display: flex; align-items: center; gap: 10px; padding: 7px 8px; font-size: .85rem; }
.mc-person:not(:last-child) { border-bottom: 1px solid #f1f3f5; }
.mc-dot { flex: 0 0 auto; width: 8px; height: 8px; border-radius: 50%; background: #ced4da; }
.mc-dot.is-on { background: #198754; }
.mc-person-main { flex: 1 1 auto; min-width: 0; display: flex; flex-direction: column; gap: 3px; }
.mc-person-name { font-weight: 600; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; display: inline-flex; align-items: center; gap: 6px; }
.mc-quiet {
  font-size: .68rem; font-weight: 700; padding: 0 6px; border-radius: 999px;
  background: #fff3cd; color: #8a5a00; border: 1px solid #ffe69c;
}
.mc-person-bar { height: 3px; max-width: 180px; border-radius: 2px; background: #f1f3f5; overflow: hidden; }
.mc-person-bar-fill { display: block; height: 100%; background: var(--sfp-indigo); opacity: .5; }
.mc-person-stat { flex: 0 0 auto; display: inline-flex; align-items: center; gap: 4px; font-size: .76rem; color: #6c757d; font-variant-numeric: tabular-nums; }
.mc-xp {
  flex: 0 0 auto; font-size: .76rem; font-weight: 700; padding: 1px 8px; border-radius: 999px;
  background: rgba(133, 64, 245, .10); color: var(--sfp-indigo-dark); font-variant-numeric: tabular-nums;
}
@keyframes mc-xp-pulse { 0% { background: rgba(133, 64, 245, .35); } 100% { background: rgba(133, 64, 245, .10); } }
.mc-xp-flash { animation: mc-xp-pulse .9s ease-out 1; }
@media (prefers-reduced-motion: reduce) { .mc-xp-flash { animation: none; } }
/* v12.0.234 (MC 2c) — the ± XP award buttons are gone (leader awards read as
   grading); .mc-award* styles removed. The People lens v2 pieces: */
.mc-people-head {
  display: flex; align-items: center; gap: 6px; padding: 4px 8px;
  border-bottom: 2px solid #e9ecef; position: sticky; top: 0; background: #fff; z-index: 1;
}
.mc-col-sort {
  display: inline-flex; align-items: center; gap: 3px; border: 0; background: none;
  padding: 4px 6px; border-radius: 6px; font-size: .78rem; font-weight: 700;
  color: #6c757d; cursor: pointer;
}
.mc-col-sort:hover { background: #f1f3f5; color: #212529; }
.mc-col-sort:focus-visible { outline: 2px solid var(--sfp-indigo); outline-offset: 1px; }
.mc-col-sort.is-sorted { color: var(--sfp-indigo-dark); background: rgba(133, 64, 245, .08); }
.mc-col-name { flex: 1 1 auto; justify-content: flex-start; }
.mc-sort-arrow { font-size: .6rem; }
.mc-col-info {
  border: 0; background: none; padding: 4px 6px; border-radius: 6px;
  color: #6c757d; cursor: pointer; line-height: 1;
}
.mc-col-info:hover, .mc-col-info[aria-expanded="true"] { color: var(--sfp-indigo-dark); background: rgba(133, 64, 245, .08); }
.mc-col-info:focus-visible { outline: 2px solid var(--sfp-indigo); outline-offset: 1px; }
.mc-legend {
  margin: 4px 8px; padding: 8px 10px; border: 1px solid #e9d8ff; border-radius: 8px;
  background: #faf7ff; font-size: .78rem; color: #495057;
  display: grid; gap: 5px;
}
.mc-legend-row { display: flex; align-items: baseline; gap: 7px; }
.mc-legend-row .bi { color: var(--sfp-indigo-dark); flex: 0 0 auto; }
button.mc-person { width: 100%; text-align: left; background: none; border: 0; cursor: pointer; }
button.mc-person:hover { background: #f8f6ff; }
button.mc-person:focus-visible { outline: 2px solid var(--sfp-indigo); outline-offset: -2px; border-radius: 6px; }
button.mc-person.is-open { background: #f4efff; }
.mc-away {
  display: inline-flex; align-items: center; gap: 3px; margin-left: 6px;
  font-size: .68rem; font-weight: 700; padding: 0 6px; border-radius: 999px;
  background: #fff3cd; color: #8a5a00; border: 1px solid #ffe69c;
}
.mc-person-detail {
  margin: 0 8px 6px 26px; padding: 8px 10px; border-left: 3px solid var(--sfp-indigo);
  background: #faf9ff; border-radius: 0 8px 8px 0; font-size: .8rem; color: #495057;
  display: grid; gap: 6px;
}
.mc-detail-away { display: flex; gap: 6px; align-items: baseline; color: #8a5a00; font-weight: 600; }
.mc-detail-stats { display: flex; flex-wrap: wrap; gap: 4px 14px; }
.mc-detail-stats .bi { margin-right: 4px; color: #6c757d; }
.mc-detail-link { font-weight: 700; text-decoration: none; color: var(--sfp-indigo-dark); justify-self: start; }
.mc-detail-link:hover { text-decoration: underline; }
.mc-more {
  display: block; width: calc(100% - 16px); margin: 6px 8px 8px; padding: 6px;
  border: 1px dashed #ced4da; border-radius: 8px; background: none;
  font-size: .8rem; font-weight: 700; color: #6c757d; cursor: pointer;
}
.mc-more:hover { border-color: var(--sfp-indigo); color: var(--sfp-indigo-dark); background: rgba(133, 64, 245, .04); }

/* v12.0.216 — the participant's own XP chip (lobby) */
.sfp-lobby-xp { background: rgba(133, 64, 245, .12); color: var(--sfp-indigo-dark); font-weight: 700; font-variant-numeric: tabular-nums; }

/* v12.0.218 — the participant "I need to step away" toggle is a <details>
   whose summary is styled as a button; the GLOBAL details-chevron (::after)
   overlapped the label's last letter. This summary is its own affordance —
   no marker, no chevron. */
.sfp-away-control > summary { list-style: none; }
.sfp-away-control > summary::-webkit-details-marker { display: none; }
.sfp-away-control > summary::after { content: none; }
/* v12.0.218's .mc-award.is-err removed in v12.0.234 with the award buttons. */

/* v12.0.235 (MC 2d) — collapsed-bar category summary chips */
.mc-sum {
  display: inline-flex; align-items: center; gap: 4px; margin-left: 8px;
  font-size: .74rem; font-weight: 600; color: #6c757d; white-space: nowrap;
}
.mc-sum .bi { font-size: .8rem; }
.mc-sum-alert, .mc-sum-away {
  display: inline-flex; align-items: center; gap: 3px; margin-left: 6px;
  padding: 0 6px; border-radius: 999px; font-weight: 700;
  background: #fff3cd; color: #8a5a00; border: 1px solid #ffe69c;
}
@media (max-width: 640px) { .mc-sum { display: none; } } /* the pills win on small screens */

/* v12.0.240 — /l/live cross-journey People section */
.mc-live-people { border-top: 2px solid #e9ecef; margin-top: 6px; padding: 8px; }
.mc-live-people-head {
  display: flex; align-items: center; gap: 6px; font-size: .78rem; font-weight: 700;
  color: #5a3d8a; margin-bottom: 6px;
}
.mc-live-people-row {
  display: flex; align-items: center; flex-wrap: wrap; gap: 6px;
  padding: 6px 8px; border-radius: 8px; text-decoration: none; color: inherit; font-size: .84rem;
}
.mc-live-people-row:hover { background: #f8f6ff; }
.mc-live-people-name { font-weight: 600; display: inline-flex; align-items: center; gap: 6px; }
.mc-live-people-count { color: #6c757d; font-size: .78rem; white-space: nowrap; }

/* v12.0.260 — Mission Control ack/snooze (AN.1) */
.mc-rowwrap { display: flex; align-items: stretch; gap: 4px; }
.mc-rowwrap .mc-row { flex: 1 1 auto; min-width: 0; }
.mc-row-actions { display: flex; flex-direction: column; justify-content: center; gap: 2px; }
.mc-act {
  border: 1px solid var(--border, #d5d2ca); background: transparent; color: inherit;
  border-radius: 6px; padding: 2px 6px; font-size: 12px; line-height: 1.4; cursor: pointer;
  opacity: 0.55; transition: opacity 120ms ease, background 120ms ease;
}
.mc-rowwrap:hover .mc-act, .mc-act:focus-visible { opacity: 1; }
.mc-act:hover { background: rgba(0,0,0,0.06); }
.mc-act:disabled { opacity: 0.35; cursor: default; }
.mc-act-fail { animation: mcActFail 0.9s ease; border-color: #c0392b; color: #c0392b; }
@keyframes mcActFail { 0%,100% { transform: none; } 25% { transform: translateX(-2px); } 75% { transform: translateX(2px); } }
.mc-suppressed { font-size: 12px; opacity: 0.6; margin-left: 6px; }

/* v12.0.262 — cross-journey People lens on /l/live */
.mc-live-people-rowwrap { display: flex; align-items: center; gap: 4px; }
.mc-live-people-rowwrap .mc-live-people-row { flex: 1 1 auto; min-width: 0; }
.mc-jppl-toggle { align-self: center; }
.mc-jppl-host { padding: 2px 0 6px 14px; display: flex; flex-direction: column; gap: 2px; }
.mc-person.is-static { cursor: default; border: 0; background: transparent; display: flex; align-items: center; gap: 8px; padding: 2px 4px; text-align: left; }

/* ═══ v12.0.357 (leader feedback L1+L2+L3) — the three Live-page panels
   (Mission Control · Learning · XP leaderboard) unified: same corner
   radius as the mc-bar (10px), the same bi-chevron-down glyph at the same
   right offset with the same 180° open-rotation, hint text that keeps
   contrast in BOTH states, and an open outline in the panel's own hue
   (the mc-panel's indigo border was already the model — the two details
   cards join it). Scoped to the two summary classes so the generic
   details ::after elsewhere is untouched. ═══ */
details.sf-collapse-green.card,
details.sf-collapse-yellow.card { border-radius: 10px; }
details.sf-collapse-green:not([open]) > summary.sf-collapse-green-summary,
details.sf-collapse-yellow:not([open]) > summary.sf-collapse-yellow-summary { border-radius: 9px; }
details.sf-collapse-green[open] > summary.sf-collapse-green-summary,
details.sf-collapse-yellow[open] > summary.sf-collapse-yellow-summary { border-radius: 9px 9px 0 0; }

/* L2 — the SAME chevron as .mc-bar-chevron: the bi glyph, colour-inherit,
   0.75rem from the right (the mc-bar's padding), 180° when open. */
details.sf-collapse-green > summary.sf-collapse-green-summary::after,
details.sf-collapse-yellow > summary.sf-collapse-yellow-summary::after {
  content: "\f282";
  font-family: "bootstrap-icons";
  font-size: 1rem;
  right: 0.75rem;
  transform: translateY(-50%);
  color: inherit;
}
details.sf-collapse-green[open] > summary.sf-collapse-green-summary::after,
details.sf-collapse-yellow[open] > summary.sf-collapse-yellow-summary::after {
  transform: translateY(-50%) rotate(180deg);
}
@media (prefers-reduced-motion: reduce) {
  details.sf-collapse-green > summary::after,
  details.sf-collapse-yellow > summary::after { transition: none; }
}

/* L1 — the hint keeps contrast in BOTH states: white on the full-colour
   collapsed header (the v341 rule), the summary's own type colour (muted)
   on the light open header — white-on-light was invisible. */
details[open] > summary .sf-collapse-hint { color: inherit; opacity: 0.75; }

/* L3 — the open card outlines in the panel's hue (the mc-panel indigo
   border is the sibling treatment). */
details.sf-collapse-green[open].card { border-color: rgba(25, 135, 84, 0.35); }
details.sf-collapse-yellow[open].card { border-color: rgba(255, 193, 7, 0.55); }

/* v12.0.358 (L6) — the live-learning rows pre-render BOTH badge + button
   variants; the row's data-visible picks one (live-learning-toggle.js
   flips the attribute after a fetch toggle — no page reload). */
#live-learning [data-ll-row][data-visible="1"] [data-ll-when-hidden] { display: none; }
#live-learning [data-ll-row][data-visible="0"] [data-ll-when-visible] { display: none; }


/* ═══ v12.0.372 (round-3 P5) — content tables (challenge + learning
   elements): slightly smaller text and honest breathing room between the
   table's outside border and the cell content. Scoped to the participant
   content wrappers so admin/leader tables are untouched. ═══ */
.sfp-page .table-responsive > .table,
.sfp-learning-body .table-responsive > .table {
  font-size: 0.875rem;
}
.sfp-page .table-responsive > .table > :not(caption) > * > th,
.sfp-page .table-responsive > .table > :not(caption) > * > td,
.sfp-learning-body .table-responsive > .table > :not(caption) > * > th,
.sfp-learning-body .table-responsive > .table > :not(caption) > * > td {
  padding: 0.45rem 0.65rem; /* table-sm's 0.25/0.25 sat flush against the border */
}


/* ═══ v12.0.372 (round-3 P6) — the sidebar can never overflow: flex
   children only truncate when they may SHRINK (min-width:0 — the classic
   flexbox truncation trap); the container clips as the last line of
   defence. Covers journey names, nicknames, and any future entry. ═══ */
#participantSidebarNav { overflow-x: hidden; }
#participantSidebarNav .nav-link.d-flex > .text-truncate,
#participantSidebarNav .d-flex > .text-truncate,
#participantSidebarNav .d-flex > strong.text-truncate {
  flex: 1 1 auto;
  min-width: 0;
}


/* ═══ v12.0.375 (round-3 L2) — the Prepare→Participants mini-blocks: the
   membership badges could overflow the card (long challenge names × many
   challenges). Each membership is now one contained row: the badge takes
   the card's width, the CHALLENGE name truncates, the group/state stays
   visible, and the zone grows downward in tidy lines. ═══ */
.sfp-pcard-memberships .sfp-pcard-badge {
  display: inline-flex;
  align-items: center;
  max-width: 100%;
  min-width: 0;
  text-align: left;
}
.sfp-pcard-memberships .sfp-pcard-ch {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  min-width: 0;
  flex: 0 1 auto;
}
.sfp-pcard-mrow { min-width: 0; }
/* v12.0.378 (leader feedback 4a) — the roster card's MC-parity stats row. */
.sfp-pcard-stats { font-size: 0.76rem; font-variant-numeric: tabular-nums; }

/* v12.0.396 (P3) — "Revise your answer" exists in the DOM whenever revising
 * is allowed, but shows only while the panel is in the answered state —
 * appearing live right after the first submit (no re-render needed) and
 * never before an answer exists. */
.sfp-chat:not(.is-answered) .sfp-chat-revise { display: none; } /* v12.0.400 — the panel class is .sfp-chat (the v396 selector matched nothing) */

/* v12.0.396 (P8 + leader L7) — on large screens both sidebars (leader and
 * participant share .sf-sidebar) were narrow (220px) with slightly
 * oversized text; widen to 280px and calm the type one notch. The matching
 * main-offset override follows the same breakpoint the base rule uses. */
@media (min-width: 992px) {
  .sf-sidebar { width: 280px; }
  .sf-main { margin-left: 280px; }
  .sf-sidebar .nav-link { font-size: 0.9rem; }
  .sf-sidebar .text-uppercase.small { font-size: 0.72rem; }
}

/* v12.0.397 — FOCUS DIM (participant): while an interactive element is
 * expanded, everything else dims behind it. The overlay sits below the
 * expanded panel (z-index) and above the rest; participant-chat.js owns
 * the element via a 2s reconciler (failsafe: the dim exists iff an
 * .is-expanded panel exists). */
#sfp-focus-dim {
  position: fixed; inset: 0;
  background: rgba(15, 23, 42, 0.45);
  z-index: 1035;
  cursor: pointer;
}
body.sfp-focus-mode .sfp-chat.is-expanded {
  position: relative;
  z-index: 1040;
  background: var(--bs-body-bg, #fff);
  border-radius: 0.5rem;
}

/* v12.0.399 — the leader's chat avatar: brand-blue filled circle with the
 * yellow XP star, matching their bubble color (all chats — the element
 * chat and both contact chats share these classes). */
.sfp-chat-avatar.is-leader {
  background: var(--sf-brand-blue, #1f4e8c);
  color: #f6c344;
  display: inline-flex;
  align-items: center;
  justify-content: center;
}
/* v14.33.0 tried 1.05em; still too small (user report, v14.37.0) — 1.3em
 * fills the circle. Applies to BOTH identity marks: the leader's yellow
 * star and Athena's bi-stars glyph (same is-leader colors — brand-blue
 * circle, yellow glyph — deliberately identical per the owner's call;
 * §5.2 honesty rides on the sender name + typing indicator). */
.sfp-chat-avatar.is-leader .bi { font-size: 1.3em; line-height: 1; }

/* v12.0.403 (GR.1) — the grading panel's identity: light-indigo "Grade"
 * header with the stars icon (the XP visual language). Same palette as the
 * participant's revise affordance, so "leader acts on an answer" reads
 * consistently across both sides. */
.responses-evalgrade-head {
  display: inline-flex; align-items: center; gap: 6px;
  color: #6f42c1; background: #f3edfb;
  border: 1px solid #e2d6f5; border-radius: 8px;
  font-size: 0.8rem; font-weight: 700;
  padding: 3px 10px; margin-bottom: 8px;
}
.responses-evalgrade-head .bi-stars { color: #f6c344; }

/* v12.0.403 (SCROLL.1) — the expand scroll targets block:"start"; this keeps
 * the panel header clear of the fixed topbar. */
.sfp-chat { scroll-margin-top: 72px; }

/* v12.0.403 (IMG.1) rule for button.sfp-img-tap-hint removed in v14.57.2
 * (IMG.2) — the hint became the round corner icon .sfp-img-zoom-btn (see the
 * Image element block). */

/* v12.0.403 (BUBBLE.1) — a submitted image-option renders as its OWN bubble
 * above the text bubble, at a readable size (was a chip-sized thumbnail
 * inline with the label). Both chat surfaces share the pattern. */
.sfp-chat-img-bubble, .responses-img-bubble {
  padding: 4px !important;
  margin-bottom: 2px;
  line-height: 0;
}
.sfp-chat-answer-img, .responses-answer-img {
  max-width: min(260px, 100%);
  max-height: 220px;
  border-radius: 10px;
  display: block;
}
.sfp-chat-img-bubble img + img, .responses-img-bubble img + img { margin-top: 4px; }

/* v12.0.406 — THE five-report Away bug: Bootstrap display utilities are
 * !important, so d-inline-flex on the topbar chip BEAT its inline
 * display:none (and every sidebar-live setShown write) since v354 — the
 * chip was structurally unhideable. Same conflict on the Learning nav
 * pair. The shown display for every inline-toggled sidebar element lives
 * HERE, scoped and non-!important: inline display:none (SSR or setShown)
 * wins when hiding; removing it reveals these. */
[data-sfp-topbar-away] { display: inline-flex; align-items: center; gap: 0.25rem; }
/* v12.0.407 (#4) — the lobby unread badge, same conflict via [hidden]:
 * utilities load AFTER the reboot's [hidden]{display:none!important}, so
 * d-inline-flex WON and "0 new" could never hide. Scoped rule + [hidden]
 * exception replace the utility. */
.sfp-lobby-unread { display: inline-flex; align-items: center; gap: 0.25rem; }
.sfp-lobby-unread[hidden] { display: none; }
/* v14.53.0 (participant report 5a) — the AWAY chip wins the topbar slot.
 * Both chips are yellow pills in the same place; showing both reads as one
 * broken control. Two classes = higher specificity than the base rule above,
 * and still non-!important so [hidden] keeps working (see v12.0.406 — a
 * Bootstrap d-* utility here would be !important and beat both). */
.sfp-lobby-unread.sfp-unread-away-suppressed { display: none; }
[data-sfp-nav-learning-head] { display: block; }
[data-sfp-nav-learning] { display: flex; }

/* v12.0.407 — the away-form validation message: was invalid-feedback +
 * d-block (utilities beat the reboot's [hidden]), so the error text could
 * never hide. Scoped display, non-!important, [hidden] wins again. */
.sfp-away-err { display: block; }

/* ── v14.11.0 — auto-respond canvas (auto-respond-canvas.js; design §4.2) ── */
#sf-ar-overlay { position: fixed; inset: 0; z-index: 1080; }
.sf-ar-modal { position: absolute; inset: 0; display: flex; flex-direction: column;
  background: var(--bs-body-bg); }
.sf-ar-head { display: flex; align-items: center; gap: 1rem; flex-wrap: wrap;
  padding: .6rem 2.9rem .6rem 1rem; /* v14.35.0 — right padding clears the pinned X */
  border-bottom: 1px solid var(--bs-border-color);
  background: var(--bs-tertiary-bg);
  position: relative; } /* v14.35.0 — anchor for the pinned close button */
.sf-ar-head-title { min-width: 0; flex: 1 1 auto; }
.sf-ar-head-prompt { display: inline-block; max-width: 40vw; overflow: hidden;
  text-overflow: ellipsis; white-space: nowrap; vertical-align: bottom; }
/* v14.35.0 (user request) — on narrow windows the header wraps; the mid
   section may wrap internally but the TOOLS keep their shape (no two-line
   "Tidy up" buttons) and the X stays pinned to the TOP-RIGHT corner instead
   of drifting next to Tidy up. */
.sf-ar-head-mid { display: flex; align-items: center; gap: .75rem; flex-wrap: wrap; row-gap: .3rem; }
.sf-ar-head-actions { position: absolute; top: .55rem; right: .8rem; display: flex; align-items: center; gap: .5rem; }
.sf-ar-toast { padding: .35rem 1rem; font-size: .85rem;
  background: var(--bs-info-bg-subtle); color: var(--bs-info-text-emphasis);
  border-bottom: 1px solid var(--bs-border-color); }
.sf-ar-body { flex: 1 1 auto; display: flex; min-height: 0; }
.sf-ar-library { width: 240px; flex: 0 0 240px; overflow-y: auto;
  border-right: 1px solid var(--bs-border-color); padding: .5rem .4rem;
  background: var(--bs-tertiary-bg); }
.sf-ar-mirror { width: 300px; flex: 0 0 300px; overflow-y: auto; /* legacy right-panel sizing — overridden for the in-aside pane below */
  border-left: 1px solid var(--bs-border-color); padding: .6rem .6rem; }
.sf-ar-shelf { margin-bottom: 1rem; }
.sf-ar-shelf-title { font-size: .8rem; font-weight: 600; margin: .2rem .2rem .35rem; }
.sf-ar-lib-filter { margin-bottom: .35rem; }
.sf-ar-shelf-body { display: flex; flex-direction: column; gap: .3rem; }
.sf-ar-lib-block { display: flex; align-items: center; gap: .4rem; width: 100%;
  text-align: left; font-size: .82rem; padding: .3rem .5rem; background: transparent;
  color: var(--bs-tertiary-color); cursor: grab; }
.sf-ar-lib-block.is-action { border: 2px solid rgba(102,16,242,.4); border-radius: 5px 999px 999px 5px; }
.sf-ar-lib-block.is-reaction { border: 2px solid rgba(32,201,151,.5); border-radius: 999px 5px 5px 999px; }
.sf-ar-lib-block:not(.is-placed):hover { color: var(--bs-body-color); background: var(--bs-body-bg); }
.sf-ar-lib-block.is-placed { opacity: .45; cursor: default; }
.sf-ar-lib-label { flex: 1 1 auto; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.sf-ar-lib-placed { font-size: .65rem; text-transform: uppercase; letter-spacing: .03em; }
.sf-ar-canvas-wrap { flex: 1 1 auto; overflow: auto; position: relative;
  background:
    linear-gradient(var(--bs-border-color-translucent) 1px, transparent 1px),
    linear-gradient(90deg, var(--bs-border-color-translucent) 1px, transparent 1px);
  background-size: 34px 34px; }
.sf-ar-canvas { position: relative; min-width: 100%; min-height: 100%; padding-bottom: 120px; }
.sf-ar-zonehint { position: sticky; top: 4px; display: inline-block; font-size: .7rem;
  text-transform: uppercase; letter-spacing: .06em; color: var(--bs-tertiary-color);
  padding: .1rem .5rem; z-index: 1; }
.sf-ar-zonehint.is-right { position: absolute; left: 702px; top: 4px; }
.sf-ar-empty { position: absolute; inset: 30% 10% auto; text-align: center;
  color: var(--bs-tertiary-color); font-size: .95rem; }
.sf-ar-svg { position: absolute; inset: 0; z-index: 1; overflow: visible; }
.sf-ar-edge { fill: none; stroke: var(--bs-secondary-color); stroke-width: 2.25;
  cursor: pointer; pointer-events: stroke; }
.sf-ar-edge:hover { stroke: var(--bs-primary); }
.sf-ar-edge.is-selected { stroke: var(--bs-danger); stroke-width: 3; }
.sf-ar-edge-x { cursor: pointer; }
.sf-ar-edge-x circle { fill: var(--bs-danger); }
.sf-ar-edge-x text { fill: #fff; font-size: 14px; font-weight: 700; }
.sf-ar-block { position: absolute; z-index: 2; display: flex; align-items: center;
  padding: .35rem 1rem .35rem .6rem; font-size: .82rem; line-height: 1.15;
  background: var(--bs-body-bg); box-shadow: 0 1px 3px rgba(0,0,0,.12); cursor: grab; }
.sf-ar-block:focus-visible { outline: 2px solid var(--bs-primary); outline-offset: 2px; }
.sf-ar-block.is-action { border: 2.5px solid rgba(102,16,242,.8);
  background: rgba(102,16,242,.08); border-radius: 6px 999px 999px 6px; } /* v14.27.0 — right side rounded: the wire leaves here */
.sf-ar-block.is-reaction { border: 2.5px solid rgba(25,135,84,.75);
  background: rgba(32,201,151,.10); border-radius: 999px 6px 6px 999px; /* v14.27.0 — left side rounded: the wire arrives here */ padding-left: 1.1rem; }
.sf-ar-block.is-unwired { border-style: dashed; border-color: var(--bs-warning);
  box-shadow: 0 0 0 3px rgba(255,193,7,.15); }
.sf-ar-block-label { flex: 1 1 auto; overflow: hidden; display: -webkit-box;
  -webkit-line-clamp: 2; -webkit-box-orient: vertical; }
.sf-ar-block-x { position: absolute; top: -8px; right: -8px; width: 20px; height: 20px;
  border-radius: 50%; border: 1px solid var(--bs-border-color); background: var(--bs-body-bg);
  color: var(--bs-secondary-color); font-size: 12px; line-height: 1; padding: 0; }
.sf-ar-block-x:hover { color: var(--bs-danger); border-color: var(--bs-danger); }
.sf-ar-port { position: absolute; width: 14px; height: 14px; border-radius: 50%;
  border: 2px solid var(--bs-body-bg); background: var(--bs-secondary-color);
  top: 50%; transform: translateY(-50%); cursor: crosshair; z-index: 3; }
.sf-ar-port.is-out { right: -8px; }
.sf-ar-port.is-in { left: -8px; background: rgba(25,135,84,.9); }
.sf-ar-block.is-action:hover .sf-ar-port.is-out,
.sf-ar-body.is-pending .sf-ar-port.is-in { background: var(--bs-primary);
  box-shadow: 0 0 0 4px rgba(13,110,253,.25); }
.sf-ar-prio { display: inline-flex; align-items: center; justify-content: center;
  width: 18px; height: 18px; margin-right: .35rem; border-radius: 50%;
  background: var(--bs-warning-bg-subtle); color: var(--bs-warning-text-emphasis);
  border: 1px solid var(--bs-warning-border-subtle); font-size: .68rem; font-weight: 700;
  flex: 0 0 auto; }
.sf-ar-mirror-list { list-style: none; margin: 0; padding: 0; display: flex;
  flex-direction: column; gap: .35rem; }
.sf-ar-mirror-row { display: flex; align-items: baseline; gap: .35rem; font-size: .82rem;
  padding: .25rem .35rem; border-radius: .3rem; background: var(--bs-tertiary-bg); }
.sf-ar-mirror-from { font-weight: 600; }
.sf-ar-mirror-to { color: var(--bs-secondary-color); }
.sf-ar-shake { animation: sfArShake .35s ease-in-out; }
@keyframes sfArShake { 0%,100%{transform:translateX(0)} 25%{transform:translateX(-4px)}
  75%{transform:translateX(4px)} }
.sf-el-autorespond .sf-ar-chip { vertical-align: middle; }
.responses-auto-badge .bi, .responses-auto-chip .bi { font-size: .9em; }
/* v14.33.0 — Athena's avatar now shares the leader's colors (brand-blue
 * circle, yellow glyph): the is-leader base styles apply unchanged. The
 * is-auto class stays as a semantic hook (tests, future styling). */
.sfp-chat-avatar.is-auto { }
@media (max-width: 900px) {
  /* v14.29.0 — the old right panel hid at 900px; Review now lives INSIDE
     the library aside and must never disappear. */
  .sf-ar-library { width: 180px; flex-basis: 180px; }
}

/* v14.14.0 — auto-respond canvas: element visibility blocks (the third
   kind). Tag silhouette (pointed left edge) = "a part of the challenge";
   sky = show, warm slate = hide, plus the icon + the word. */
.sf-ar-block.is-elact { border-radius: 10px; padding-left: .6rem; } /* v14.28.0 — square with rounded corners, no arrow clip */
.sf-ar-block.is-elact.is-show { background: #e7f6fd; border: 2.5px solid #4aa8d8; }
.sf-ar-block.is-elact.is-hide { background: #f4efe9; border: 2.5px solid #b08d57; }
.sf-ar-elact-kind { display: inline-flex; align-items: center; gap: 3px; font-size: .62rem; font-weight: 700; letter-spacing: .06em; margin-right: 6px; }
.sf-ar-block.is-elact.is-show .sf-ar-elact-kind { color: #1e6f96; }
.sf-ar-block.is-elact.is-hide .sf-ar-elact-kind { color: #7a5a2e; }
.sf-ar-eye { width: 13px; height: 13px; fill: currentColor; }
.sf-ar-selfnote { font-size: .68em; opacity: .7; font-style: italic; }
.sf-ar-elshelf-row { display: flex; align-items: center; justify-content: space-between; gap: 6px; padding: 3px 8px; }
.sf-ar-elshelf-label { font-size: .78rem; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; max-width: 9.5rem; }
.sf-ar-elshelf-btns { display: inline-flex; gap: 4px; flex: none; }
.sf-ar-elshelf-btn { font-size: .68rem; padding: 1px 7px; border-radius: 4px; border: 1px solid; background: #fff; cursor: pointer; }
.sf-ar-elshelf-btn.is-show { border-color: #4aa8d8; color: #1e6f96; }
.sf-ar-elshelf-btn.is-hide { border-color: #b08d57; color: #7a5a2e; }
.sf-ar-elshelf-btn:disabled { opacity: .45; cursor: default; }
.sf-ar-mirror-elact { display: inline-block; padding: 0 5px; border-radius: 4px; font-size: .85em; }
.sf-ar-mirror-elact.is-show { background: #e7f6fd; color: #1e6f96; }
.sf-ar-mirror-elact.is-hide { background: #f4efe9; color: #7a5a2e; }
.sf-ar-tools { display: inline-flex; gap: 3px; margin-left: 8px; flex-wrap: nowrap; } /* v14.35.0 — buttons never deform */
.sf-ar-tools .btn { padding: 0 .5rem; line-height: 1.4; white-space: nowrap; }
/* v14.35.0 — the single collapsed "How this works" legend above Build/Review
   (replaces the two (i) tips, by user request). */
.sf-ar-howto summary { cursor: pointer; font-size: .8rem; font-weight: 600;
  color: var(--bs-secondary-color); user-select: none; }
.sf-ar-howto[open] summary { margin-bottom: .25rem; }
.sf-ar-howto-body { font-size: .78rem; color: var(--bs-secondary-color);
  padding: 0 .15rem; }
.sf-ar-active-exp { margin-left: 4px; }
.sf-ar-zoombox { position: relative; overflow: hidden; }
.sf-ar-offscreen-hint { position: sticky; bottom: 8px; margin-left: auto; margin-right: 12px; width: max-content;
  background: #212529; color: #fff; font-size: .74rem; padding: 3px 10px; border-radius: 999px; opacity: .92; z-index: 5; }

/* v14.15.0 — Phase 2b: the catch-all block ("Anything else"). Dashed =
   "everything not drawn here"; the asterisk carries into the label. */
.sf-ar-block.is-else { border-style: dashed; background: #f6f4fb; font-style: italic; }
.sf-ar-else-entry { padding: 4px 8px 8px; }
.sf-ar-else-btn { font-size: .78rem; padding: 2px 9px; border-radius: 6px; border: 1px dashed #7a5fb8; color: #4d3d80; background: #faf8ff; cursor: pointer; }
.sf-ar-else-btn:disabled { opacity: .45; cursor: default; }

/* v14.16.0 — auto-respond canvas: the repair banner (detached wiring). */
.sf-ar-repair { background: #fff6e0; border: 1px solid #e3c26b; color: #6b5417; font-size: .8rem;
  padding: 6px 12px; border-radius: 6px; margin: 6px 10px 2px; }

/* v14.23.0 — the combo junction (ALL OF): a distinct diamond-ish action block. */
.sf-ar-block.is-combo { background: #eef2ff; border: 2px solid #6672d8; border-radius: 14px;
  font-weight: 600; color: #3843a0; }
.sf-ar-block.is-combo.is-incomplete { border-style: dashed; opacity: .8; }
.sf-ar-block.is-combo .sf-ar-port.is-in { left: -7px; right: auto; }

/* v14.26.0 — the or/and toggle circles: OR orange, AND indigo — twins. */
.sf-ar-orand-anchor { position: absolute; z-index: 5; }
.sf-ar-orand { display: inline-flex; align-items: center; justify-content: center;
  width: 34px; height: 34px; border-radius: 50%; border: 2px solid #e8590c;
  background: #fff4ec; color: #c2410c; font-size: 11px; font-weight: 800;
  line-height: 1; cursor: pointer; letter-spacing: .02em; padding: 0; }
.sf-ar-orand:hover { background: #e8590c; color: #fff; }
.sf-ar-orand.is-and { position: static; margin-left: 6px; border-color: #4f46e5;
  background: #4f46e5; color: #fff; }
.sf-ar-orand.is-and:hover { background: #3730a3; border-color: #3730a3; }

/* v14.26.0 — Athena's opener: indigo, filled. */
.sf-ar-open-btn { background: #4f46e5; border: 1px solid #4f46e5; color: #fff; }
.sf-ar-open-btn:hover { background: #3730a3; border-color: #3730a3; color: #fff; }
.sf-ar-open-btn:disabled { background: #a5b4fc; border-color: #a5b4fc; color: #fff; }

/* v14.26.0 — collapsible form sections (native details) */
.sf-collapse-section > summary { cursor: pointer; list-style: none; }
/* v14.29.0 — one affordance only: the "edit ⌄" hint (the ::before caret
   plus the native marker made three chevrons). */
.sf-collapse-section > summary::marker { content: ""; }
.sf-collapse-section > summary::-webkit-details-marker { display: none; }

/* v14.26.0 — canvas info tips + the collapsible Rules panel */
.sf-ar-info { display: inline-block; position: relative; }
.sf-ar-info > summary { list-style: none; cursor: pointer; color: #6c757d; display: inline-block; }
.sf-ar-info > summary::-webkit-details-marker { display: none; }
.sf-ar-info > summary::marker { content: ""; } /* v14.30.0 — the visible "^" beside (i) was the STANDARD marker */
.sf-ar-info > summary:hover { color: #4f46e5; }
.sf-ar-info[open] > .sf-ar-info-pop { position: absolute; z-index: 40; top: 1.5rem; left: 0;
  min-width: 240px; max-width: 320px; background: #fff; border: 1px solid #d0d5dd;
  border-radius: .5rem; box-shadow: 0 8px 24px rgba(16,24,40,.12); padding: .6rem .75rem;
  font-size: .78rem; color: #344054; line-height: 1.45; }
.sf-ar-mirror-details > summary { cursor: pointer; list-style: none; }
.sf-ar-mirror-details > summary::-webkit-details-marker { display: none; }
.sf-ar-mirror-details > summary::after { content: "\25B8"; float: right; color: #98a2b3; transition: transform .15s; }
.sf-ar-mirror-details[open] > summary::after { transform: rotate(90deg); }

/* v14.27.0 — dragging: the block and its wires stay fully visible; the
   live connector while wiring. */
.sf-ar-block.is-dragging { opacity: 1; box-shadow: 0 6px 18px rgba(16,24,40,.25); z-index: 6; }
.sf-ar-edge.is-live { stroke-dasharray: 6 5; opacity: .8; }

/* v14.28.0 — the Card visual summary thumbnail + affordance */
.sf-cvm-thumb { width: 56px; height: 40px; border-radius: 6px; display: inline-flex;
  align-items: center; justify-content: center; font-size: 18px; color: #fff;
  border: 1px solid rgba(0,0,0,.15); overflow: hidden; flex: 0 0 auto; }
.sf-cvm-thumb.sf-grad-none { background: linear-gradient(135deg, #6c757d, #adb5bd); }
.sf-cvm-thumb img { width: 100%; height: 100%; object-fit: cover; }
.sf-collapse-hint .bi { transition: transform .15s; }
.sf-collapse-section[open] .sf-collapse-hint .bi { transform: rotate(180deg); }
.sf-collapse-section[open] .sf-collapse-hint { visibility: hidden; }

/* v14.28.0 — the sidebar's two states + the Review dot */
.sf-ar-lib-tabs .btn { font-size: .78rem; }
.sf-ar-tabdot { display: inline-block; width: 8px; height: 8px; border-radius: 50%;
  background: #fd7e14; vertical-align: middle; margin-left: 3px; }
.sf-ar-libpane.sf-ar-mirror { padding: 0 .25rem; }

/* v14.29.0 — the in-aside Review pane: full aside width, wrapping rows
   (the legacy 300px fixed width overflowed under the canvas). */
.sf-ar-libpane.sf-ar-mirror { width: auto; flex: none; overflow: visible; border: 0; }
.sf-ar-libpane .sf-ar-mirror-row { flex-wrap: wrap; min-width: 0; }
.sf-ar-libpane .sf-ar-mirror-from, .sf-ar-libpane .sf-ar-mirror-to { min-width: 0; overflow-wrap: anywhere; }
/* v14.30.0 — info pops inside the aside are INLINE EXPANDOS: the open
   details becomes a full-width block and the pop flows in place, pushing
   content down. No overlay, no clipping, no z-index fights — the previous
   left/right:0 resolved against the tiny inline (i) and rendered a
   one-word-per-line column. */
.sf-ar-library .sf-ar-info[open] { display: block; width: 100%; flex-basis: 100%; }
.sf-ar-library .sf-ar-info[open] > .sf-ar-info-pop { position: static; display: block;
  margin-top: .35rem; min-width: 0; max-width: none; box-shadow: none;
  background: var(--bs-tertiary-bg); border: 1px solid var(--bs-border-color); }
.sf-ar-lib-top { flex-wrap: wrap; }
.sf-ar-library { position: relative; z-index: 3; }

/* v14.31.0 — the canvas modal footer (exercise-edit pattern) */
.sf-ar-foot { display: flex; align-items: center; gap: .5rem; padding: .5rem .75rem;
  border-top: 1px solid var(--bs-border-color); background: var(--bs-body-bg); flex: 0 0 auto; }
.sf-ar-head-actions .btn-close { font-size: .8rem; }

/* v14.31.0 — Athena typing indicator (three bouncing dots) */
.sfp-typing { display: flex; align-items: center; gap: .5rem; padding: .35rem .25rem; }
.sfp-typing-who { font-size: .75rem; font-weight: 600; color: var(--bs-secondary-color); }
.sfp-typing-dots { display: inline-flex; gap: .25rem; padding: .45rem .7rem;
  background: var(--bs-tertiary-bg); border-radius: 1rem; }
.sfp-typing-dots i { width: .42rem; height: .42rem; border-radius: 50%;
  background: var(--bs-secondary-color); display: inline-block;
  animation: sfp-typing-bounce 1.2s infinite ease-in-out; }
.sfp-typing-dots i:nth-child(2) { animation-delay: .15s; }
.sfp-typing-dots i:nth-child(3) { animation-delay: .3s; }
@keyframes sfp-typing-bounce {
  0%, 60%, 100% { transform: translateY(0); opacity: .45; }
  30% { transform: translateY(-.28rem); opacity: 1; }
}

/* v14.33.0 - the progress breakdown header rows are fully clickable */
.sfp-progress-breakrow.sfp-break-clickable { cursor: pointer; }
.sfp-progress-breakrow.sfp-break-clickable:hover { background: var(--bs-tertiary-bg); }

/* ── v14.36.0 — admin Projects table (ground-up redesign, user request) ──
   Flat, compact, filterable; built for 100+ rows. Sticky header keeps the
   columns readable on a long scroll; the icon buttons in the journeys cell
   stay finger-sized without inflating row height. */
.sf-projects-table thead th {
  position: sticky;
  top: 0;
  background: var(--bs-body-bg);
  z-index: 1;
}
.sf-projects-table td { vertical-align: middle; }
.sf-projects-journey + .sf-projects-journey { margin-top: 2px; }

/* v14.37.0 — the paste-JSON disclosure's chevron rotates open/closed. */
.sf-paste-details summary::marker { content: ""; }
.sf-paste-details summary::-webkit-details-marker { display: none; }
.sf-paste-details .sf-paste-chevron { transition: transform .15s ease; display: inline-block; }
.sf-paste-details[open] .sf-paste-chevron { transform: rotate(90deg); }


/* v14.41.0 (leader request) — the auto-response badge beside the role badge
   on a challenge card (Design / Prepare / Live) and in the exercise list.
   Light indigo ground with an indigo outline, matching the role badge's size
   and pill shape so the pair reads as one row of facts about the exercise,
   while the colour marks it as a different KIND of fact: the role badge says
   who sees what, this one says the exercise answers by itself. */
.sfp-badge-auto {
  background: #eae6fd;
  border: 1px solid #6610f2;
  color: #3d0a91;
}

/* v14.41.0 (leader request) — the waiting-room disclosure. Bootstrap puts
   .collapsed on the toggler while the panel is shut, so the chevron points
   down when there is more to see and up when it is open. */
.sfp-waiting-toggle .bi-chevron-down {
  transition: transform .15s;
}
.sfp-waiting-toggle:not(.collapsed) .bi-chevron-down {
  transform: rotate(180deg);
}

/* ── v14.59.2 (reports 6+7) — a dimmed row must not dim its ACTIONABLE
   buttons. A closed/skipped challenge is faded to say "this is done", which
   is right, but the delete button (design mode) and the reopen button
   (prepare mode) are fully clickable and dimming them said otherwise.
   opacity is inherited and cannot be "undone" on a child, so the rule works
   the other way round: the ROW keeps full opacity and the fade is applied to
   its text content only, leaving buttons at full strength with a real
   pointer cursor. Selectors verified against the markup in
   _journey-design-mode.ejs (li.challenge-design-row) and
   _journey-prepare-mode.ejs (li.challenge-row). */
li.challenge-design-row.opacity-50,
li.challenge-row.opacity-50 {
  opacity: 1 !important;
}
li.challenge-design-row.opacity-50 span:not(.visually-hidden),
li.challenge-design-row.opacity-50 small,
li.challenge-row.opacity-50 span:not(.visually-hidden),
li.challenge-row.opacity-50 small {
  opacity: 0.55;
}
li.challenge-design-row.opacity-50 button,
li.challenge-row.opacity-50 button,
li.challenge-design-row.opacity-50 button span,
li.challenge-row.opacity-50 button span {
  opacity: 1;
}
li.challenge-design-row.opacity-50 button:not(:disabled),
li.challenge-row.opacity-50 button:not(:disabled) {
  cursor: pointer;
}

/* v15.0.2 (leader report 1) — the "Leader — multi-respond" label is yellow,
   so the manual mode is distinguishable from Athena's at a glance in a long
   element list. On the strip's dark chip, warning-yellow carries enough
   contrast; the icon stays inherited so the two never disagree. */
.sf-ar-multi-label { color: var(--bs-warning, #ffc107); font-weight: 600; }

/* v15.8.1 — fenced code blocks from renderMarkdown (the Hidden Factory card) */
.sf-md-code {
  background: var(--bs-tertiary-bg);
  border: 1px solid var(--bs-border-color);
  font-size: 0.85em;
  overflow-x: auto;
  white-space: pre;
}
.sf-md-code code { color: var(--bs-emphasis-color); background: transparent; padding: 0; }

/* ═══ v15.9.0 (owner request 3) — the MODE-COLOURED PAGE ═══════════════════
 * The space OUTSIDE the outline/challenge cards takes the mode's colour
 * (design=yellow, prepare=green, live=blue), so which mode you're in is
 * unmistakable at any scroll position. The content panes sit on a card
 * surface so everything inside stays exactly as readable as before. Header
 * text above the mode band: dark on yellow, white on green/blue (the
 * owner's legibility rule). Tints via color-mix keep dark mode sane. */
body[data-cockpit-mode="design"]  { background: color-mix(in srgb, var(--bs-warning) 32%, var(--bs-body-bg)); }
body[data-cockpit-mode="prepare"] { background: color-mix(in srgb, var(--bs-success) 45%, var(--bs-body-bg)); }
body[data-cockpit-mode="live"]    { background: color-mix(in srgb, var(--bs-primary) 45%, var(--bs-body-bg)); }

/* the content panes become cards on the coloured page */
body[data-cockpit-mode] .cockpit-mode {
  background: var(--bs-body-bg);
  border-radius: 0.5rem;
  padding: 1rem;
  box-shadow: 0 1px 3px rgba(0,0,0,0.12);
}

/* v15.10.0 (owner request 2) — header text back to DARK on every mode
 * (the white-on-green/blue rule of v15.9.0 reversed on sight). The classes
 * stay in the markup for future tuning. */
body[data-cockpit-mode] .sf-journey-header,
body[data-cockpit-mode] .sf-journey-header-sub,
body[data-cockpit-mode] a.sf-journey-header-sub {
  color: var(--bs-body-color);
}

/* v16.3.0 — .calc-table (the calc viz table shape), previously UNSTYLED
   (dataviz standards finding F4): Few's table rules — numbers right-aligned
   in tabular figures, text left, NO vertical rules, whitespace as the
   separator, one stronger rule under the header, totals separated + bold.
   Scope: calc content elements only (docs/data-visualization-standards.md
   §A.2); never platform chrome. */
.calc-table { border-collapse: collapse; width: auto; font-variant-numeric: tabular-nums; }
.calc-table th, .calc-table td { padding: 0.35rem 0.75rem; border: 0; text-align: left; }
.calc-table thead th { border-bottom: 2px solid var(--calc-grid, #e5e7eb); color: var(--calc-muted, #6b7280); font-weight: 600; }
.calc-table tbody tr + tr td { border-top: 1px solid var(--calc-grid, #e5e7eb); }
.calc-table td.num, .calc-table th.num { text-align: right; }
.calc-table tr.totals td { border-top: 2px solid var(--calc-grid, #e5e7eb); font-weight: 700; }

/* v16.14.0 — show-your-work (Stage H): the derivation block a calc element
   appends when its author sets display.show_work. Quiet by default; the
   summary is the tap target; print surfaces open it. */
/* v16.39.0 (owner report 8) — flow / network holders: the picture keeps its
   natural width and the HOLDER scrolls sideways (8b: it was cut at the block's
   right edge); the orientation button is a block UNDER the picture, never
   beside it (8a: "Show left to right" sat next to a tall vertical map). */
/* v16.40.0 (owner report round 2, #4) — the participant sidebar's entries carry
   a colour each so they are told apart at a glance: journey light blue,
   Learning light green, My progress light yellow; unread counts under CONTACT
   are red badges with a white number (set in the markup: text-bg-danger). The
   section headings above them stay as they were. */
.sfp-nav-journey, .sfp-nav-journey .bi { color: #9fd8ff !important; }
.sfp-nav-journey .badge.text-bg-light { background-color: #9fd8ff !important; color: #0b3d5c !important; }
.sfp-nav-learning, .sfp-nav-learning .bi { color: #b7f0c2 !important; }
.sfp-nav-learning .badge.text-bg-light { background-color: #b7f0c2 !important; color: #0f4a1e !important; }
.sfp-nav-progress, .sfp-nav-progress .bi { color: #ffe9a3 !important; }
.sfp-nav-progress .badge.text-bg-light { background-color: #ffe9a3 !important; color: #5c4400 !important; }
/* v16.41.0 (round 3, 2b) — a settings sub-section that is not usable yet (the Templates entry is off) */
.sfp-disabled { opacity: .55; }
.sfp-autosave-status:empty { display: none; }
.sf-calc-scroll { overflow-x: auto; overflow-y: hidden; max-width: 100%; -webkit-overflow-scrolling: touch; }
.sf-calc-scroll > svg { display: block; max-width: none; }
.sf-calc-block > svg { display: block; max-width: 100%; height: auto; }
/* v16.40.0 (round 2, #1) — a visual opens full screen on tap (viz-zoom.js) */
.sf-calc-block svg { cursor: zoom-in; }
.sf-derivation, [data-calc-dials] { cursor: auto; }
.sf-calc-flip { display: block; }
.sf-derivation { margin: .35rem 0 0; font-size: .85em; color: var(--calc-muted, #6b7280); }
.sf-derivation > summary { cursor: pointer; list-style: none; display: flex; flex-wrap: wrap; gap: .4em; align-items: baseline; }
.sf-derivation > summary::-webkit-details-marker { display: none; }
.sf-derivation-result { font-weight: 600; color: var(--calc-ink, #1f2937); }
.sf-derivation-state { padding: 0 .4em; border-radius: .6em; background: var(--calc-grid, #e5e7eb); }
.sf-derivation-under { color: var(--calc-warn, #b45309); }
.sf-derivation-hint { text-decoration: underline dotted; }
.sf-derivation[open] .sf-derivation-hint { display: none; }
.sf-derivation-body { margin: .35rem 0 0 .25rem; padding-left: .6rem; border-left: 2px solid var(--calc-grid, #e5e7eb); }
.sf-derivation-section { margin-top: .4rem; text-transform: uppercase; letter-spacing: .04em; font-size: .8em; }
.sf-derivation-inputs { margin: .15rem 0 0; display: grid; grid-template-columns: max-content 1fr; gap: .1rem .6rem; }
.sf-derivation-inputs dt, .sf-derivation-inputs dd { margin: 0; }
.sf-derivation-source, .sf-derivation-n { opacity: .8; }
.sf-derivation-expr { display: inline-block; margin-top: .15rem; }
.sf-derivation-because { margin: .15rem 0 0; color: var(--calc-ink, #1f2937); }
@media print { .sf-derivation-body { display: block; } .sf-derivation-hint { display: none; } }

/* v17.6.0 — the calc dials panel (owner round 8). Two homes: the DOCK wraps
   the panel in the flow and holds its height for good (no flashing); FLOATING
   is a bottom sheet the same node moves into. Colours are the calc palette:
   cream paper, the number teal for the thumbs. Corners match the calc cards.
   Header: chevron left, ✕ right, with room; no hint text. */
.sf-calc-dials-dock { display: block; }
.sf-calc-dials { background: var(--calc-paper, #fcf4e4); border: 1px solid var(--calc-grid, #e6e0d3); border-radius: var(--sfp-radius, 14px); padding: .6rem .85rem .75rem; margin-bottom: 1rem; }
.sf-calc-dials-head { display: flex; align-items: center; gap: .5rem; min-height: 2rem; }
.sf-calc-dials-title { flex: 0 0 auto; color: var(--calc-fill, #177891); }
.sf-calc-dials-summary { flex: 1 1 auto; min-width: 0; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; font-size: .85rem; display: none; }
.sf-calc-dials-chip { display: inline-block; margin-right: .6rem; }
.sf-calc-dials-toggle, .sf-calc-dials-close { flex: 0 0 auto; display: none; width: 2rem; height: 2rem; padding: 0; line-height: 2rem; text-align: center; border: 0; background: transparent; color: var(--calc-fill, #177891); font-size: 1rem; border-radius: .5rem; }
.sf-calc-dials-toggle:hover, .sf-calc-dials-close:hover { background: rgba(23,120,145,.10); }
.sf-calc-dials-close { margin-left: auto; }
.sf-calc-dials-body label { margin-bottom: .35rem !important; }
.sf-calc-dials .form-range { margin-top: .1rem; }
.sf-calc-dials .form-range::-webkit-slider-thumb { background: var(--calc-fill, #177891); }
.sf-calc-dials .form-range::-moz-range-thumb { background: var(--calc-fill, #177891); }
.sf-calc-dials .form-range:focus::-webkit-slider-thumb { box-shadow: 0 0 0 .25rem rgba(23,120,145,.25); }
.sf-calc-dials .form-range::-webkit-slider-runnable-track { background: rgba(23,120,145,.18); }
.sf-calc-dials .form-range::-moz-range-track { background: rgba(23,120,145,.18); }
.sf-calc-dials.is-floating {
  position: fixed; left: 0; right: 0; bottom: 0; z-index: 1040; margin: 0 !important;
  border-radius: 1rem 1rem 0 0; border-width: 1px 0 0 0; box-shadow: 0 -.5rem 1.5rem rgba(0,0,0,.18);
  max-height: 38vh; overflow-y: auto; -webkit-overflow-scrolling: touch;
  padding-bottom: calc(.5rem + env(safe-area-inset-bottom));
}
.sf-calc-dials.is-floating .sf-calc-dials-head { position: sticky; top: 0; background: var(--calc-paper, #fcf4e4); z-index: 1; margin: -.6rem -.85rem .25rem; padding: .35rem .5rem 0 .5rem; }
.sf-calc-dials.is-floating .sf-calc-dials-toggle, .sf-calc-dials.is-floating .sf-calc-dials-close { display: inline-block; }
.sf-calc-dials.is-floating.is-collapsed .sf-calc-dials-body { display: none; }
.sf-calc-dials.is-floating.is-collapsed .sf-calc-dials-summary { display: inline; }
.sf-calc-dials.is-floating.is-collapsed { max-height: none; }
.sf-calc-dials.is-floating.is-collapsed .sf-calc-dials-head { cursor: pointer; margin-bottom: -.25rem; }
@media (min-width: 768px) {
  .sf-calc-dials.is-floating { left: auto; right: 1rem; bottom: 1rem; width: 400px; border-radius: var(--sfp-radius, 14px); border-width: 1px; max-height: 50vh; }
}
/* v18.12.0 — the reserve is the sheet's measured height (set once by learning-calc.js), on every width: the last
   content scrolls clear of the sheet on a laptop too (owner, 1b: "the lowest content hides behind the sliders") */
body.sf-calc-dials-floating { padding-bottom: calc(var(--sf-dials-reserve, 38vh) + 1rem); }
/* the elements a drag moves, for the moment after the move: yellow, then gone */
.sf-calc-driven-pulse { outline: 3px solid var(--calc-area-warn, #f9ba14); outline-offset: 3px; border-radius: .75rem; transition: outline-color .7s ease; }
.sf-calc-driven-pulse.is-fading { outline-color: transparent; }

/* v17.6.0 — calc elements (owner round 8): centred; charts take the block's
   full width; consecutive tiles sit two abreast; a tile fills its card. */
.sf-calc-block { text-align: center; }
.sf-calc-block > svg, .sf-calc-block .sf-viz > svg { display: block; width: 100% !important; max-width: none !important; height: auto; margin: 0 auto; }
.sf-calc-block > svg.sf-tile { max-width: 100% !important; }
.sf-calc-block .sf-viz { max-width: none; }
.sf-calc-block .sf-viz > div { text-align: left; }
.sf-calc-caption { margin: .4rem 0 0; font-size: .95rem; }
.sf-calc-caption-label { color: var(--bs-secondary-color); }
.sf-calc-caption-value { font-size: 1.25rem; margin: 0 .25rem; }
.sf-calc-caption-n { color: var(--calc-warn, #8f6904); font-size: .85rem; }
.sf-calc-block .sf-derivation { text-align: left; }
/* v17.7.0 (owner round 9, #1) — two abreast at EVERY width, the phone included: the tile is HTML now and reflows */
.sf-calc-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 0 .75rem; }
@media (min-width: 576px) { .sf-calc-grid { gap: 0 1rem; } }
.sfp-content.sf-calc-dials-card > .sfp-content-body { padding: 0 !important; }
.sfp-content.sf-calc-dials-card .sf-calc-dials { margin-bottom: 0; }
/* the HTML tile */
.sf-tile-h { background: var(--calc-paper, #fcf4e4); border: 1px solid var(--calc-grid, #e6e0d3); border-radius: var(--sfp-radius, 14px); padding: .85rem 1rem .9rem; text-align: left; }
.sf-tile-h-label { color: var(--bs-secondary-color); font-size: .9rem; line-height: 1.25; overflow-wrap: break-word; }
.sf-tile-h-value { font-weight: 700; font-size: clamp(1.4rem, 6.2vw, 2.1rem); line-height: 1.15; margin-top: .3rem; overflow-wrap: anywhere; font-variant-numeric: tabular-nums; }
.sf-tile-h-unit { font-weight: 600; font-size: .7em; }
.sf-tile-h-state { font-size: .9rem; margin-top: .3rem; }
.sf-tile-h-n { color: var(--bs-secondary-color); font-size: .8rem; margin-top: .2rem; }
.sf-tile-h-warn { color: var(--calc-warn, #8f6904); }
.sf-tile-h-why { color: var(--bs-secondary-color); font-size: .85rem; margin-top: .2rem; }
@media (min-width: 576px) { .sf-tile-h-value { font-size: clamp(1.6rem, 3.2vw, 2.3rem); } }
.sfp-content.sf-calc-card-tile .sf-tile-h { border: 0; border-radius: var(--sfp-radius, 14px); height: 100%; }
.sf-calc-grid > .sfp-content { min-width: 0; }
.sfp-content.sf-calc-card-tile > .sfp-content-body { padding: 0 !important; } /* the cream tile IS the card */
.sfp-content.sf-calc-card-tile { overflow: hidden; }
.sfp-content.sf-calc-card-tile .sf-derivation { padding: .5rem .85rem .75rem; }
.sfp-content.sf-calc-card-tile .sfp-content-heading { padding: .6rem .85rem 0; }

/* v17.8.0 — dial shapes, presets, play, inline sparklines */
.sf-calc-dials-presets { display: flex; flex-wrap: wrap; gap: .35rem; margin: 0 0 .6rem; }
.sf-calc-preset { border-color: var(--calc-fill, #177891); color: var(--calc-fill, #177891); }
.sf-calc-preset:hover, .sf-calc-preset:focus { background: var(--calc-fill, #177891); color: #fff; border-color: var(--calc-fill, #177891); }
.sf-calc-dial-playrow { display: flex; align-items: center; gap: .5rem; }
.sf-calc-dial-playrow .form-range { flex: 1 1 auto; }
.sf-calc-dial-play { flex: 0 0 auto; width: 2rem; height: 2rem; padding: 0; line-height: 2rem; border: 1px solid var(--calc-fill, #177891); color: var(--calc-fill, #177891); background: transparent; border-radius: .5rem; }
.sf-calc-dial-play[aria-pressed="true"] { background: var(--calc-fill, #177891); color: #fff; }
.sf-calc-dials select.sf-calc-dial, .sf-calc-dials input[type="date"].sf-calc-dial { margin-top: .15rem; }
.sf-calc-inline-viz { display: inline-block; vertical-align: middle; margin: 0 .15rem; }
.sf-calc-inline-viz > svg { height: 1.25em; width: auto; vertical-align: middle; }

/* v18.0.0 (X1) — the post-it board (participant) */
.sf-place-hint { margin-bottom: .5rem; }
.sf-place-deck { display: flex; flex-wrap: wrap; gap: .5rem; min-height: 3.4rem; padding: .5rem; border: 2px dashed var(--calc-grid, #e6e0d3); border-radius: var(--sfp-radius, 14px); background: var(--calc-paper, #fcf4e4); }
.sf-place-card { display: flex; align-items: center; justify-content: center; text-align: center; width: 9.5rem; min-height: 3.4rem; padding: .45rem .55rem; border: 0; border-radius: .35rem; background: #fff59d; color: #212529; box-shadow: 1px 2px 4px rgba(0,0,0,.18); font: inherit; font-size: .85rem; line-height: 1.25; cursor: grab; user-select: none; touch-action: manipulation; position: relative; }
.sf-place-card[data-color="green"] { background: #c8e6c9; } .sf-place-card[data-color="orange"] { background: #ffcc80; } .sf-place-card[data-color="blue"] { background: #bbdefb; } .sf-place-card[data-color="pink"] { background: #f8bbd0; }
.sf-place-card.is-picked { outline: 3px solid var(--calc-fill, #177891); outline-offset: 2px; }
.sf-place-card.is-dragging { opacity: .6; }
.sf-place-card:disabled { cursor: default; opacity: .9; }
.sf-place-card.is-right { outline: 3px solid var(--calc-good, #2e7e35); } .sf-place-card.is-wrong { outline: 3px solid var(--calc-bad, #c64511); } .sf-place-card.is-key { outline: 3px dashed var(--calc-fill, #177891); background: #fff; }
.sf-place-card .sf-place-mark { position: absolute; top: -.55rem; right: -.55rem; width: 1.4rem; height: 1.4rem; border-radius: 50%; color: #fff; font-size: .8rem; display: flex; align-items: center; justify-content: center; }
.sf-place-card.is-right .sf-place-mark { background: var(--calc-good, #2e7e35); } .sf-place-card.is-wrong .sf-place-mark { background: var(--calc-bad, #c64511); }
.sf-place-table { border-collapse: separate; border-spacing: .4rem; width: auto; min-width: 100%; margin-top: .6rem; }
/* v18.3.0 (owner 2a) — the board scrolls sideways when the columns do not fit (a 25-column table on a phone); cells never shrink below their min-width */
.sf-place-scroll { overflow-x: auto; overflow-y: hidden; max-width: 100%; -webkit-overflow-scrolling: touch; }
.sf-place-card.is-kept { cursor: default; }
/* v18.3.0 (owner 6a) — the marked board inside a leader's thread bubble: smaller cells, no grab cursor, cards fit the cell */
/* v18.12.0 — the review page in a full-screen modal (owner 2d); the three-way close dialog (2b); the shared note as one quiet line (2h) */
.sf-place-key-modal { width: min(1500px, 98vw); height: 94vh; display: flex; flex-direction: column; }
.sf-place-key-modal-body { flex: 1 1 auto; overflow: auto; padding: 1rem; background: var(--bs-body-bg); }
.sf-place-key-tools { display: flex; justify-content: flex-end; }
.sf-place-editor.sf-place-scroll { overflow-x: auto; }
.sf-place-review-modal { width: min(1400px, 98vw); height: 94vh; display: flex; flex-direction: column; }
.sf-place-review-frame { flex: 1 1 auto; width: 100%; border: 0; background: #fff; }
.sf-place-close-dialog { width: min(560px, 96vw); }
.sf-place-close-dialog .sfp-chartmodal-body { color: var(--bs-body-color); }
.sf-place-shared-note { font-size: .8rem; opacity: .85; }
/* v18.10.0 — a BRANCHING flow: columns of boxes, an SVG overlay for the arrows (place-flow.js); the linear row keeps its 18.0 rules */
.sf-place-flow.is-branching { position: relative; display: flex; flex-wrap: nowrap; align-items: stretch; gap: 3rem; padding: 1.2rem .5rem .75rem; overflow: visible; min-width: max-content; }
.sf-place-flow.is-branching .sf-place-flow-col { display: flex; flex-direction: column; justify-content: center; gap: 1rem; }
.sf-place-flow.is-branching .sf-place-slot { position: relative; z-index: 1; margin: 0; }
.sf-place-flow-arrows { position: absolute; inset: 0; width: 100%; height: 100%; pointer-events: none; z-index: 0; overflow: visible; }
.sf-place-flow-edge { fill: none; stroke: #4e6b8a; stroke-width: 2; }
.sf-place-flow-head { stroke: #4e6b8a; fill: none; } /* v18.13.0 — an open chevron, as the Deep Cause Mapping canvas draws it */
/* v18.13.0 (owner 2b/2c) — a flow scrolls sideways when it outgrows its area, and a slot GROWS with the cards in it */
.sf-place-flow.is-branching { overflow-x: auto; overflow-y: visible; align-items: stretch; }
/* v18.14.0 (owner 4) — the reader's own choice: top-to-bottom instead of left-to-right (columns become rows) */
.sf-place-flow.is-vertical { flex-direction: column; overflow-x: visible; min-width: 0; }
.sf-place-flow.is-vertical .sf-place-flow-col { flex-direction: row; justify-content: flex-start; }
.sf-place-flow.is-vertical .sf-place-arrow { transform: rotate(90deg); }
.sf-place-flow.is-branching .sf-place-flow-col { flex: 0 0 auto; }
.sf-place-flow.is-branching .sf-place-slot { height: auto; }
/* v18.14.0 — the review card's own scroll box wins over anything the board sets inside it */
.sf-place-review-scroll { overflow-x: auto; overflow-y: visible; max-width: 100%; }
.sf-place-review-scroll .sf-place-flow.is-branching { overflow-x: visible; } /* the outer box scrolls, not the flow */
.sf-place-image { overflow-x: auto; }
/* v18.4.0 — the post-it CANVAS (place-canvas.js): the flow's boxes + arrows, the picture with draggable spots */
.sf-place-canvas { width: min(1100px, 96vw); max-height: 92vh; display: flex; flex-direction: column; }
.sf-place-canvas-body { overflow: auto; padding: 1rem; flex: 1 1 auto; color: var(--bs-body-color); }
.sf-place-canvas-foot { padding: .6rem 1rem; border-top: 1px solid var(--bs-border-color); }
.sf-place-canvas-flow { display: flex; align-items: center; gap: .5rem; overflow-x: auto; padding: .75rem .25rem 1rem; min-height: 8rem; }
.sf-place-canvas-box { flex: 0 0 auto; width: 11rem; border: 2px solid #4e6b8a; border-radius: .5rem; background: #f7f3e8; padding: .5rem; cursor: grab; box-shadow: 1px 2px 4px rgba(0,0,0,.12); }
.sf-place-canvas-box:active { cursor: grabbing; }
.sf-place-canvas-box-text { font-weight: 600; text-align: center; background: transparent; border-color: transparent; }
.sf-place-canvas-box-text:focus { background: #fff; }
.sf-place-canvas-box-tools { display: flex; justify-content: space-between; margin-top: .3rem; }
.sf-place-canvas-box-tools .btn { min-width: 2rem; min-height: 1.8rem; }
.sf-place-canvas-arrow { font-size: 1.6rem; color: #4e6b8a; flex: 0 0 auto; }
/* v18.10.0 — the arrow tool: the armed source, the eligible targets, the arrow chips */
.sf-place-canvas-box.is-arrow-source { outline: 3px solid #0d6efd; }
.sf-place-canvas-box.is-arrow-target { cursor: crosshair; outline: 2px dashed #0d6efd66; }
/* v18.12.0 — ports (Athena's idiom): ● out on the right, ○ in on the left; the × chip on every arrow; the rubber band */
.sf-place-canvas-box { position: relative; }
.sf-place-port { position: absolute; top: 50%; width: 14px; height: 14px; margin-top: -7px; border-radius: 50%; border: 2px solid #4e6b8a; background: #fff; z-index: 2; }
.sf-place-port.is-in { left: -8px; }
.sf-place-port.is-out { right: -8px; background: #4e6b8a; cursor: crosshair; touch-action: none; }
.sf-place-port.is-out:hover, .sf-place-port.is-out.is-armed { background: #0d6efd; border-color: #0d6efd; box-shadow: 0 0 0 4px rgba(13,110,253,.25); }
.sf-place-canvas-box.is-drop-target { outline: 3px solid #0d6efd; outline-offset: 2px; }
.sf-place-edge-chip { position: absolute; transform: translate(-50%, -50%); z-index: 3; width: 20px; height: 20px; border-radius: 50%; border: 1px solid #b02a37; background: #fff; color: #b02a37; font-size: 14px; line-height: 16px; padding: 0; cursor: pointer; }
.sf-place-edge-chip:hover { background: #b02a37; color: #fff; }
.sf-place-flow-edge.is-band { stroke-dasharray: 6 4; stroke: #0d6efd; }
.sf-place-band { pointer-events: none; }
.sf-place-canvas-flow.is-branching { gap: 3rem; }
.sf-place-canvas-flow.is-branching .sf-place-canvas-box { position: relative; z-index: 1; }
.sf-place-canvas-add { flex: 0 0 auto; min-height: 3rem; }
.sf-place-canvas-picture { position: relative; display: inline-block; max-width: 100%; line-height: 0; cursor: crosshair; user-select: none; }
.sf-place-canvas-picture[data-empty] { cursor: default; display: block; }
.sf-place-canvas-img { max-width: 100%; height: auto; display: block; border-radius: .35rem; }
.sf-place-canvas-spot { position: absolute; transform: translate(-50%, -50%); display: flex; align-items: center; gap: .25rem; line-height: 1.2; cursor: default; }
.sf-place-canvas-spot-dot { width: 1.8rem; height: 1.8rem; border-radius: 50%; background: #d9480f; color: #fff; font-weight: 700; font-size: .8rem; display: inline-flex; align-items: center; justify-content: center; cursor: grab; box-shadow: 0 0 0 3px rgba(255,255,255,.85); touch-action: none; }
.sf-place-canvas-spot-text { width: 8rem; font-size: .75rem; padding: .15rem .35rem; background: rgba(255,255,255,.92); }
.sf-place-canvas-spot .btn { line-height: 1; }
.sf-place-image-placeholder { border: 2px dashed var(--bs-border-color); border-radius: .5rem; min-height: 9rem; display: flex; align-items: center; justify-content: center; gap: .4rem; color: var(--bs-secondary-color); font-size: .9rem; background: rgba(0,0,0,.02); }
/* v18.3.0 (owner 6d) — the results modal: the chart modal's frame, a readable (not projector) body; the rocket cluster a touch narrower to make room for the results button */
.sf-live-calc-modal-body { overflow-y: auto; padding: 1rem; color: var(--bs-body-color); }
.reveal-row .reveal-thread-cluster { min-width: 4rem; }
.reveal-row .reveal-calc-open, .reveal-row .reveal-place-open { min-height: 44px; min-width: 2.6rem; display: inline-flex; align-items: center; justify-content: center; border-radius: 8px; flex-shrink: 0; margin-left: 0.25rem; }
.sf-place-mini .sf-place-card { cursor: default; font-size: .75rem; min-height: 2.4rem; padding: .3rem .4rem; width: auto; }
.sf-place-mini .sf-place-cell { min-width: 6rem; height: auto; min-height: 3.2rem; }
.sf-place-mini .sf-place-table th { font-size: .75rem; } .sf-place-mini .sf-place-table th[scope="row"] { font-size: .8rem; } /* v18.3.0 (owner 1d) — a card carried over from the previous round: right, locked */
.sf-place-table th { font-weight: 600; color: var(--bs-secondary-color); font-size: .85rem; text-align: left; }
.sf-place-table th[scope="row"] { color: var(--bs-body-color); font-size: 1rem; }
.sf-place-cell { vertical-align: top; min-width: 8rem; height: 5.2rem; border: 2px dashed var(--calc-grid, #e6e0d3); border-radius: var(--sfp-radius, 14px); background: #fff; padding: .35rem; transition: background .15s, border-color .15s; }
.sf-place-cell .sf-place-card, .sf-place-slot .sf-place-card, .sf-place-spot .sf-place-card { width: auto; margin: .25rem 0; }
[data-place-zone].is-over, .has-pick [data-place-zone]:not([data-place-zone="deck"]):hover { background: #e7f3f6; border-color: var(--calc-fill, #177891); }
.sf-place-flow { display: flex; align-items: flex-start; gap: .4rem; flex-wrap: wrap; margin-top: .6rem; }
/* v18.13.0 (owner 2c) — height: auto + a MIN: a slot grows with the cards dropped in it and shrinks again */
.sf-place-slot { flex: 1 1 9rem; min-height: 5.2rem; height: auto; align-self: stretch; border: 2px dashed var(--calc-grid, #e6e0d3); border-radius: var(--sfp-radius, 14px); background: #fff; padding: .35rem; }
.sf-place-slot-label { font-size: .8rem; color: var(--bs-secondary-color); }
.sf-place-arrow { color: var(--bs-secondary-color); font-size: 1.4rem; align-self: center; }
.sf-place-image { margin-top: .6rem; }
.sf-place-image-img { display: block; width: 100%; height: auto; border-radius: var(--sfp-radius, 14px); }
.sf-place-spot { position: absolute; transform: translate(-50%, -50%); min-width: calc(var(--r, 5%) * 2); min-height: 2.6rem; border: 2px dashed rgba(23,120,145,.7); border-radius: .75rem; background: rgba(255,255,255,.75); padding: .2rem; }
.sf-place-spot-label { font-size: .7rem; color: var(--calc-fill, #177891); display: block; text-align: center; }
.sf-place-status { margin: .4rem 0 0; }
@media (max-width: 575px) { .sf-place-card { width: 7.8rem; font-size: .8rem; } .sf-place-cell { min-width: 5.5rem; height: 4.6rem; } }
