/* === Reset & Base === */
*, *::before, *::after {
  margin: 0;
  padding: 0;
  box-sizing: border-box;
}

[hidden] {
  display: none !important;
}

:root {
  /* ── HomeFit Studio Design Tokens ─────────────────────────────── */
  /* Mirrors docs/design/project/tokens.json — single source of truth. */
  /* Decisions applied: D-01, D-03, D-04, D-07, D-09, D-10.            */
  /* D-06 (canonical state treatments) is a per-screen refactor.       */

  /* ── Brand (coral — single accent) ── */
  --c-brand: #FF6B35;
  --c-brand-dark: #E85A24;
  --c-brand-light: #FF8F5E;
  --c-brand-surface: #FFF3ED;
  --c-brand-tint-bg: rgba(255, 107, 53, 0.12);       /* D-10 */
  --c-brand-tint-border: rgba(255, 107, 53, 0.30);   /* D-10 */
  /* Coral rail — used by the lobby's tree-branch circuit grouping
     (3 px wide pseudo-element pieces concatenating across header +
     rows). Slightly punchier than the tint-border so the line reads
     as a positive piece of structure rather than a divider. */
  --c-coral-rail: rgba(255, 107, 53, 0.85);

  /* ── Surfaces — dark (D-02 semantic names) ── */
  --c-surface-bg: #0F1117;
  --c-surface-base: #1A1D27;
  --c-surface-raised: #242733;
  --c-surface-border: #2E3140;

  /* ── Surfaces — light mirror (D-08; defined but unused) ── */
  --c-surface-light-bg: #FAFAF7;
  --c-surface-light-base: #FFFFFF;
  --c-surface-light-raised: #F5F5F0;
  --c-surface-light-border: #E5E7EB;

  /* ── Ink (D-01: secondary bumped to #9CA3AF; old #6B7280 becomes muted) ── */
  --c-ink-primary: #F0F0F5;
  --c-ink-secondary: #9CA3AF;
  --c-ink-muted: #6B7280;
  --c-ink-disabled: #4B5563;

  /* ── Semantic (D-03 full-word names) ── */
  --c-success: #22C55E;
  --c-warning: #F59E0B;
  --c-error: #EF4444;
  /* Rest periods use sage green (mirrors AppColors.rest on mobile). */
  --c-rest: #86EFAC;
  --c-rest-tint-bg: rgba(134, 239, 172, 0.15);
  --c-rest-tint-border: rgba(134, 239, 172, 0.55);
  /* Legacy slate rest — kept for backward compatibility. */
  --c-rest-slate: #64748B;

  /* ── Radii ── */
  --radius-sm: 8px;
  --radius-md: 12px;
  --radius-lg: 16px;
  --radius-xl: 20px;

  /* ── Wave 21 — vertical rep-block stack ── */
  --rep-stack-rest-height: 14px;

  /* ── Focus ring (D-07: only surviving shadow token) ── */
  --c-focus-ring: 0 0 0 3px rgba(255, 107, 53, 0.30);

  /* ── Motion (D-04) ── */
  --dur-fast: 150ms;
  --dur-normal: 250ms;
  --dur-slow: 400ms;
  --ease-standard: cubic-bezier(0.2, 0, 0, 1);
  --ease-emphasized: cubic-bezier(0.16, 1, 0.3, 1);
  --ease-linear: linear;

  /* ── Typography families ── */
  --f-display: 'Montserrat', -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
  --f-body: 'Inter', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
  --f-mono: "JetBrains Mono", "SF Mono", SFMono-Regular, ui-monospace, Menlo, monospace;

  /* ── Typography scale (D-09 — mirrors tokens.json typography.scale) ── */
  --t-display-lg:  800 57px/1.05 var(--f-display);
  --t-display-md:  700 45px/1.1  var(--f-display);
  --t-display-sm:  700 36px/1.15 var(--f-display);
  --t-headline-lg: 700 32px/1.2  var(--f-display);
  --t-headline-md: 700 28px/1.25 var(--f-display);
  --t-headline-sm: 600 24px/1.3  var(--f-display);
  --t-title-lg:    700 20px/1.35 var(--f-display);
  --t-title-md:    600 16px/1.4  var(--f-body);
  --t-title-sm:    600 14px/1.4  var(--f-body);
  --t-body-lg:     400 16px/1.5  var(--f-body);
  --t-body-md:     400 14px/1.5  var(--f-body);
  --t-body-sm:     400 12px/1.5  var(--f-body);
  --t-label-lg:    600 14px/1.4  var(--f-body);
  --t-label-md:    600 12px/1.4  var(--f-body);
  --t-label-sm:    600 11px/1.4  var(--f-body);

  /* ── Safe areas ── */
  --safe-bottom: env(safe-area-inset-bottom, 0px);
  --safe-top: env(safe-area-inset-top, 0px);
}

html {
  font-family: var(--f-body);
  font-size: 16px;
  line-height: 1.5;
  color: var(--c-ink-primary);
  background: var(--c-surface-bg);
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  -webkit-text-size-adjust: 100%;
  overflow: hidden;
  height: 100%;
}

body {
  height: 100%;
  overflow: hidden;
  position: fixed;
  width: 100%;
  touch-action: pan-y;
  background: var(--c-surface-bg);
  color: var(--c-ink-primary);
}

/* === Loading Screen === */
.loading-screen {
  position: fixed;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  background: var(--c-surface-bg);
  z-index: 100;
  transition: opacity var(--dur-normal) var(--ease-standard);
}

.loading-screen.fade-out {
  opacity: 0;
  pointer-events: none;
}

.loading-content {
  text-align: center;
}

.loading-mark {
  width: 72px;
  height: 50px;
  margin: 0 auto 16px;
  color: var(--c-brand);
  display: flex;
  align-items: center;
  justify-content: center;
  animation: pulse 1.5s ease infinite;
}

.loading-mark .pulse-mark {
  width: 100%;
  height: 100%;
}

@keyframes pulse {
  0%, 100% { opacity: 1; }
  50% { opacity: 0.5; }
}

.loading-text {
  color: var(--c-ink-secondary);
  font-size: 14px;
  letter-spacing: 0.01em;
}

/* === Error Screen === */
.error-screen {
  position: fixed;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  background: var(--c-surface-bg);
  z-index: 90;
  padding: 32px;
}

.error-content {
  text-align: center;
  max-width: 300px;
}

.error-icon {
  width: 48px;
  height: 48px;
  margin: 0 auto 16px;
  border: 2px solid var(--c-surface-border);
  border-radius: 50%;
  display: flex;
  align-items: center;
  justify-content: center;
  font-weight: 600;
  font-size: 20px;
  color: var(--c-ink-secondary);
}

.error-title {
  font-family: var(--f-display);
  font-size: 18px;
  font-weight: 700;
  margin-bottom: 8px;
  color: var(--c-ink-primary);
}

.error-text {
  font-size: 14px;
  color: var(--c-ink-secondary);
  line-height: 1.6;
}

/* === App Layout ================================================== */
#app {
  display: flex;
  flex-direction: column;
  height: 100%;
  padding-top: var(--safe-top);
  padding-bottom: var(--safe-bottom);
  position: relative;
  background: var(--c-surface-bg);
}

/* === Plan bar (row 1) ============================================
   Plan title + client subtitle. Stays horizontal with the
   no-longer-rendered progress label; the label element is kept
   around for legacy-markup safety but never displays. */
.plan-bar {
  flex-shrink: 0;
  padding: 14px 20px 6px;
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 12px;
  background: var(--c-surface-bg);
}

.plan-bar-left {
  display: flex;
  flex-direction: column;
  gap: 2px;
  min-width: 0;
}

.plan-title {
  font-family: var(--f-display);
  font-size: 20px;
  font-weight: 700;
  letter-spacing: -0.025em;
  color: var(--c-ink-primary);
  line-height: 1.2;
}

.header-client {
  font-size: 13px;
  color: var(--c-ink-secondary);
  font-weight: 500;
}

/* Progress label retired; the progress-pill matrix carries position. */
.progress-label { display: none !important; }

/* === Workout timeline strip (row 3 — Wave 19.3 sits just above matrix)
   Thin row with the start wall-clock left-aligned, the live remaining
   total centred, and the projected finish wall-clock right-aligned, all
   connected by hairline rules. Pre-start the edges show "--:--" and the
   centre shows "0:00" so the row doesn't reflow on kickoff. */
.workout-timeline-bar {
  flex-shrink: 0;
  display: flex;
  flex-direction: row;
  align-items: center;
  gap: 10px;
  padding: 2px 16px 4px;
  font-family: var(--f-mono);
  font-size: 11px;
  letter-spacing: 0.1px;
  font-variant-numeric: tabular-nums;
  line-height: 1;
  color: var(--c-ink-muted);
  white-space: nowrap;
  user-select: none;
  -webkit-user-select: none;
  background: var(--c-surface-bg);
}

.workout-timeline-start,
.workout-timeline-end {
  font-weight: 500;
  color: var(--c-ink-secondary);
  flex-shrink: 0;
}

/* Centred live remaining-total — the workout headline number. Coral +
   slightly heavier weight so it outranks the two wall-clock bookends. */
.workout-timeline-total {
  flex-shrink: 0;
  font-weight: 700;
  font-size: 12px;
  color: var(--c-brand);
  letter-spacing: 0.2px;
}
.workout-timeline-total.is-prep-flashing {
  animation: prep-flash 1s steps(2, end) infinite;
}

.workout-timeline-rule {
  flex: 1 1 auto;
  height: 1px;
  background: linear-gradient(
    to right,
    transparent 0%,
    var(--c-border-soft, rgba(255, 255, 255, 0.08)) 20%,
    var(--c-border-soft, rgba(255, 255, 255, 0.08)) 80%,
    transparent 100%
  );
}

/* === Progress-pill matrix (row 3) ================================
   Horizontally scrolling grid of pills (one per exercise slide). */

.progress-matrix {
  position: relative;
  flex-shrink: 0;
  margin: 2px 0 6px;
  /* Inner flex-distributes to fit viewport. Horizontal scroll is a fallback
     for extreme cases (>50 pills × 6px min still overflows). */
  overflow-x: auto;
  overflow-y: hidden;
  touch-action: pan-y;
  -webkit-user-select: none;
  user-select: none;
}

/* Left-edge fade */
.progress-matrix::before {
  content: "";
  position: absolute;
  top: 0;
  bottom: 0;
  left: 0;
  width: 40px;
  pointer-events: none;
  z-index: 3;
  background: linear-gradient(
    to right,
    var(--c-surface-bg) 0%,
    rgba(15, 17, 23, 0) 100%
  );
}

.progress-matrix-inner {
  display: flex;
  flex-wrap: nowrap;
  gap: 8px;
  align-items: start;
  justify-content: center;
  padding: 2px 16px;
  transition: transform 300ms var(--ease-emphasized);
  will-change: transform;
}

.progress-matrix-inner.is-scrubbing {
  transition: none;
}

.matrix-col {
  display: grid;
  gap: 8px;
  align-items: start;
  grid-auto-rows: min-content;
  /* Fit-to-width: columns flex-distribute inside .progress-matrix-inner.
     Pills shrink to 6px min on long plans, then the outer falls back to
     scroll. Wave 19 bumped max-width 22 → 32 so singles render as proper
     rectangles instead of near-round squares on short sessions. */
  flex: 1 1 0;
  min-width: 6px;
  max-width: 32px;
}

/* ── Row-first circuit block (Wave 19) ─────────────────────────────────
   A circuit renders as ONE block whose children are coral-tinted rows,
   each row = one round (all exercises L→R). The block flex-distributes
   alongside .matrix-col singles so the matrix still fits the viewport
   width. Inside the block, rows stack; each row is a grid with
   N_exercises equal columns. */
.matrix-circuit {
  /* Horizontal overhead = (N-1)×4px row-gap only (block + row paddings
     retired in favour of a ::before coral tint). Put the overhead in
     flex-basis so N growth shares match N singles exactly — circuit
     pills and single pills end up pixel-identical widths.
     Max-width mirrors .matrix-col's 32px ceiling. */
  flex: var(--circuit-cols, 1) 1 calc(4px * (var(--circuit-cols, 1) - 1));
  min-width: calc(6px * var(--circuit-cols, 1) + 4px * (var(--circuit-cols, 1) - 1));
  max-width: calc(32px * var(--circuit-cols, 1) + 4px * (var(--circuit-cols, 1) - 1));
  display: flex;
  flex-direction: column;
  /* Gap between round-rows matches the horizontal pill gap so the
     circuit reads as a clean grid. */
  gap: 4px;
}

.matrix-circuit-row {
  /* Padding retired: pills sit at the row's natural edges, so the first
     row of any circuit top-aligns with single-pill rows in the same
     flex line. The coral tint band is rendered by ::before, which
     extends 2px vertically + 4px horizontally so pills still feel
     nested inside a capsule without offsetting them. */
  position: relative;
  display: grid;
  /* Equal 1fr columns by default (non-fullscreen). Reads --circuit-cols
     from the parent .matrix-circuit for the column count. Fullscreen
     swaps in --row-template-fs below for duration-weighted widths. */
  grid-template-columns: repeat(var(--circuit-cols, 1), 1fr);
  gap: 4px;
  align-items: center;
  padding: 0;
}
.matrix-circuit-row::before {
  content: "";
  position: absolute;
  inset: -2px -4px;
  background: var(--c-brand-tint-bg);
  border-radius: var(--radius-md);
  z-index: 0;
  pointer-events: none;
}
.matrix-circuit-row > .pill {
  position: relative;
  z-index: 1;
}

/* === Pill primitives ============================================= */

.pill {
  position: relative;
  background: var(--c-surface-raised);
  border: 1px solid var(--c-surface-border);
  border-radius: var(--radius-md);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 4px;
  overflow: hidden;
  color: var(--c-ink-secondary);
  font-family: var(--f-body);
  font-weight: 600;
  /* Standalone pills are direct flex children of .progress-matrix-inner via
     their parent .matrix-col (flex: 1 1 0). Pills themselves fill the column
     horizontally so width is entirely governed by the column's flex share. */
  width: 100%;
  transition: transform 150ms var(--ease-standard),
              border-color 150ms var(--ease-standard);
  -webkit-tap-highlight-color: transparent;
  cursor: pointer;
}

/* Tier classes control HEIGHT + font only — width is flex-driven via
   .matrix-col (1 1 0, capped 22px, floor 6px). */
.pill.size-spacious { height: 22px; font-size: 10px; letter-spacing: 0.5px; }
.pill.size-medium   { height: 20px; font-size: 0; }
.pill.size-dense    { height: 18px; font-size: 0; }

.pill .pill-content {
  position: relative;
  z-index: 2;
  display: flex;
  align-items: center;
  gap: 4px;
  opacity: 0.6;
  transition: opacity 200ms var(--ease-standard);
  pointer-events: none;
}

.pill .pill-icon {
  width: 14px;
  height: 14px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
}

.pill.size-medium .pill-icon { width: 16px; height: 16px; }
.pill.size-dense .pill-content { display: none; }

.pill .pill-fill {
  position: absolute;
  top: 0;
  left: 0;
  bottom: 0;
  width: 0%;
  background: rgba(255, 107, 53, 0.85);
  z-index: 1;
  transition: width 120ms linear;
}

.pill.is-active {
  border-color: var(--c-brand);
  border-width: 2px;
  margin: -1px 0;
  animation: pill-pulse 1.4s cubic-bezier(0.4, 0, 0.6, 1) infinite;
}
.pill.is-active .pill-content { opacity: 1; color: var(--c-ink-primary); }

.pill.is-active.is-paused { animation-play-state: paused; }
.pill.is-active.is-paused .pill-fill { transition: none; }

.pill.is-completed .pill-content { opacity: 0.4; color: var(--c-ink-muted); }
.pill.is-completed .pill-fill {
  width: 100% !important;
  background: var(--c-brand);
  opacity: 1;
}

/* Lobby scroll-fill (Wave Lobby-PillFill, 2026-05-05) — pills fill 100%
   as the user scrolls through the lobby's row list, drain on scroll-back.
   For circuits, all rounds of an exercise share the same row.dataset.id
   so they cross the threshold together (Carl: "for circuits all sets
   must fill up"). Distinct from .is-completed (which the deck uses for
   actually-completed workout progress). */
.pill.is-filled .pill-fill {
  width: 100%;
}

.pill.is-rest {
  color: var(--c-rest);
  background: var(--c-rest-tint-bg);
  border-color: var(--c-rest-tint-border);
}
.pill.is-rest .pill-fill { background: rgba(134, 239, 172, 0.65); }
.pill.is-rest.is-active {
  border-color: var(--c-rest);
  animation-name: pill-pulse-rest;
}
.pill.is-rest.is-active .pill-fill { background: rgba(134, 239, 172, 0.85); }
.pill.is-rest.is-completed .pill-fill {
  background: var(--c-rest);
  opacity: 1;
}

.pill.is-scrubbed {
  transform: scale(1.06);
  border-color: var(--c-brand-light);
}

/* Pill duration label — Wave 19.2.
   Each pill carries its estimated duration as "NNs"; the active pill's
   text is replaced by the live remaining-seconds countdown.
   Colour inversion trick: the glyph is painted from a gradient that
   mirrors the pill's own --fill-pct custom property, so text over the
   empty (dark) portion reads white and text over the filled (coral)
   portion reads black. Single element, no double-layer DOM. */
.pill .pill-duration {
  position: absolute;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  z-index: 3;
  font-family: var(--f-mono, ui-monospace, SFMono-Regular, monospace);
  font-size: 10px;
  font-weight: 700;
  font-variant-numeric: tabular-nums;
  letter-spacing: 0;
  line-height: 1;
  white-space: nowrap;
  pointer-events: none;
  background: linear-gradient(
    to right,
    #000 0%,
    #000 var(--fill-pct, 0%),
    #FFF var(--fill-pct, 0%),
    #FFF 100%
  );
  -webkit-background-clip: text;
          background-clip: text;
  color: transparent;
}
/* Smaller tier pills get a slightly smaller digit so the label doesn't
   clip. Dense tier drops the digit entirely — the pill is too narrow
   for legible text at that size. */
.pill.size-medium .pill-duration { font-size: 9px; }
.pill.size-dense .pill-duration { display: none; }
/* Rest pills ride a sage fill — same black/white inversion works
   because sage is light enough to read black on. */

@keyframes pill-pulse {
  0%, 100% { box-shadow: 0 0 0 0 rgba(255, 107, 53, 0.40); }
  50%      { box-shadow: 0 0 0 8px rgba(255, 107, 53, 0); }
}
@keyframes pill-pulse-rest {
  0%, 100% { box-shadow: 0 0 0 0 rgba(134, 239, 172, 0.40); }
  50%      { box-shadow: 0 0 0 8px rgba(134, 239, 172, 0); }
}

/* Scale the glow down at dense tier so it stays proportional on narrow pills. */
@keyframes pill-pulse-dense {
  0%, 100% { box-shadow: 0 0 0 0 rgba(255, 107, 53, 0.40); }
  50%      { box-shadow: 0 0 0 4px rgba(255, 107, 53, 0); }
}
@keyframes pill-pulse-rest-dense {
  0%, 100% { box-shadow: 0 0 0 0 rgba(134, 239, 172, 0.40); }
  50%      { box-shadow: 0 0 0 4px rgba(134, 239, 172, 0); }
}
.pill.size-dense.is-active { animation-name: pill-pulse-dense; }
.pill.size-dense.is-rest.is-active { animation-name: pill-pulse-rest-dense; }

.glyph-body {
  stroke: currentColor;
  stroke-width: 1.5;
  fill: none;
  stroke-linecap: round;
  stroke-linejoin: round;
}

.glyph-body .glyph-head { fill: currentColor; stroke: none; }

.progress-matrix-chevron {
  position: absolute;
  top: 50%;
  left: 10px;
  transform: translateY(-50%);
  width: 24px;
  height: 24px;
  border-radius: 50%;
  background: var(--c-brand-tint-bg);
  border: 1px solid var(--c-brand-tint-border);
  display: flex;
  align-items: center;
  justify-content: center;
  color: var(--c-brand);
  animation: pill-pulse 1.4s cubic-bezier(0.4, 0, 0.6, 1) infinite;
  z-index: 4;
  pointer-events: none;
}
.progress-matrix-chevron svg { width: 14px; height: 14px; }

/* === Active-slide header (row 4) =================================
   Single-line row: "{exercise name} · {decoded grammar}". Name weight
   is carried by the display-font treatment; grammar is part of the
   same string separated by " · ". Truncates with ellipsis if the
   string overflows the viewport width — never wraps. */
.active-slide-header {
  flex-shrink: 0;
  padding: 6px 20px 10px;
  background: var(--c-surface-bg);
  text-align: center;
}

.active-slide-title {
  font-family: var(--f-display);
  font-size: 18px;
  font-weight: 700;
  letter-spacing: -0.02em;
  color: var(--c-ink-primary);
  line-height: 1.25;
  margin: 0;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.active-slide-title.is-rest {
  color: var(--c-rest);
}

/* Embedded surface (Flutter WebView preview) — practitioner already
   knows the exercise name from Studio context, the floating title is
   redundant chrome. Gate driven by `body.is-embedded`, set in app.js
   based on `window.isHomefitEmbedded()` (HomefitBridge presence). */
body.is-embedded .active-slide-title {
  display: none;
}

/* === Card Viewport — the hero region ============================= */
.card-viewport {
  flex: 1;
  overflow: hidden;
  position: relative;
  padding: 0;
  touch-action: pan-y;
  background: #000;
}

.card-track {
  display: flex;
  height: 100%;
  transition: transform var(--dur-slow) var(--ease-emphasized);
  will-change: transform;
}

.card-track.is-swiping {
  transition: none;
}

/* === Exercise Card =============================================== */
.exercise-card {
  flex: 0 0 100%;
  width: 100%;
  display: flex;
  flex-direction: column;
  height: 100%;
}

.card-inner {
  flex: 1;
  display: flex;
  flex-direction: column;
  background: #000;
  overflow: hidden;
}

/* === Media Area ================================================== */
.card-media {
  flex: 1;
  min-height: 0;
  background: #0A0C12;
  display: flex;
  align-items: center;
  justify-content: center;
  position: relative;
  overflow: hidden;
}

.card-media img,
.card-media video {
  width: 100%;
  height: 100%;
  object-fit: contain;
  display: block;
}

/* === Photo rotation wrapper (Wave 28) ==============================
   Photos use the same wrapper-anchored rotation pattern as videos —
   transform sits on .media-rotation-wrap so the inner <img> keeps a
   clean stacking context (matches the .video-loop-pair shape).
   Defaults to a no-op on legacy plans (no inline transform → identical
   layout to today). */
.media-rotation-wrap {
  position: relative;
  width: 100%;
  height: 100%;
  display: block;
}
.media-rotation-wrap img {
  width: 100%;
  height: 100%;
  object-fit: contain;
  display: block;
}

/* === Dual-video crossfade pair (Wave 19.7) =========================
   Two stacked <video> elements share the same source. The "active"
   one sits at opacity:1, the inactive at opacity:0; on a loop seam
   we flip data-active and let the 200ms transition do the visible
   crossfade. Both elements are absolutely positioned so they
   overlap pixel-perfect. The wrapper inherits the parent
   .card-media's flex sizing. */
.video-loop-pair {
  position: relative;
  width: 100%;
  height: 100%;
  display: block;
}
.video-loop-pair .video-loop-slot {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: contain;
  display: block;
  opacity: 0;
  transition: opacity var(--loop-crossfade-fade-ms, 200ms) ease;
  will-change: opacity;
}
.video-loop-pair .video-loop-slot[data-active="true"] {
  opacity: 1;
  z-index: 2;
}
.video-loop-pair .video-loop-slot[data-active="false"] {
  z-index: 1;
}

/* === Play/pause toggle (top-right of card-viewport) ==============
   YouTube-style icon-swap button. Same visual chrome as fullscreen +
   settings. Hidden outside workout mode (the tap-to-pause on the
   media area still handles preview interactions). */
.playpause-toggle {
  position: absolute;
  top: 12px;
  right: 12px;
  width: 40px;
  height: 40px;
  border: none;
  border-radius: 50%;
  background: rgba(0, 0, 0, 0.55);
  color: var(--c-brand);
  padding: 0;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  cursor: pointer;
  z-index: 20;
  transition: background 150ms ease, transform 100ms ease, opacity 180ms ease;
  -webkit-tap-highlight-color: transparent;
}
.playpause-toggle[hidden] {
  display: none;
}
.playpause-toggle:hover {
  background: rgba(0, 0, 0, 0.75);
}
.playpause-toggle:active {
  transform: scale(0.92);
}
.playpause-toggle svg {
  width: 20px;
  height: 20px;
  fill: currentColor;
  stroke: none;
}
.playpause-toggle svg:not([hidden]) {
  display: block;
}
.playpause-toggle .pp-icon-play {
  /* Optical-balance the play triangle against the circular bg. */
  margin-left: 3px;
}
.playpause-toggle:focus-visible {
  outline: 2px solid var(--c-brand);
  outline-offset: 2px;
}

/* === Landscape pre-workout Maximise pill =========================
   Hidden everywhere by default. The landscape media query below
   shows it only when (orientation: landscape) AND (max-height: 540)
   AND not in workout mode AND not already fullscreen. Tap reclaims
   viewport from Safari's persistent landscape chrome (URL bar +
   tab bar + bottom toolbar). */
.landscape-maximise-pill {
  display: none;
}

/* === Fullscreen toggle (top-right of card-viewport, BELOW playpause) === */
.fullscreen-toggle {
  position: absolute;
  top: 60px;
  right: 12px;
  width: 40px;
  height: 40px;
  border: none;
  border-radius: 50%;
  background: rgba(0, 0, 0, 0.55);
  color: var(--c-brand);
  padding: 0;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  cursor: pointer;
  z-index: 20;
  transition: background 150ms ease, transform 100ms ease, opacity 180ms ease;
  -webkit-tap-highlight-color: transparent;
}
.fullscreen-toggle:hover {
  background: rgba(0, 0, 0, 0.75);
}
.fullscreen-toggle:active {
  transform: scale(0.92);
}
.fullscreen-toggle svg {
  width: 20px;
  height: 20px;
  display: block;
  stroke: currentColor;
}
.fullscreen-toggle:focus-visible {
  outline: 2px solid var(--c-brand);
  outline-offset: 2px;
}

/* === Settings toggle (bottom of the right-edge chrome stack) ======
   Per-device playback preferences (Milestone P + the Wave 19.5
   "Show me" override). The right-edge column occupies a 40px
   column so the video stays uncluttered when not maximised. */
.settings-toggle {
  position: absolute;
  top: 108px;
  right: 12px;
  width: 40px;
  height: 40px;
  border: none;
  border-radius: 50%;
  background: rgba(0, 0, 0, 0.55);
  color: var(--c-brand);
  padding: 0;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  cursor: pointer;
  z-index: 20;
  transition: background 150ms ease, transform 100ms ease, opacity 180ms ease;
  -webkit-tap-highlight-color: transparent;
}
.settings-toggle:hover {
  background: rgba(0, 0, 0, 0.75);
}
.settings-toggle:active {
  transform: scale(0.92);
}
.settings-toggle svg {
  width: 20px;
  height: 20px;
  display: block;
  stroke: currentColor;
}
.settings-toggle[aria-expanded="true"] {
  background: rgba(0, 0, 0, 0.85);
}
.settings-toggle:focus-visible {
  outline: 2px solid var(--c-brand);
  outline-offset: 2px;
}

/* Right-edge chrome column: [playpause top:12], [fullscreen top:60],
   [settings top:108]. Each rule pins its own coords above. The
   standalone mute speaker was retired in Wave 42 — gear panel only. */

/* === Settings popover ============================================= */
.settings-popover {
  position: absolute;
  /* Anchored just below the gear button. The gear sits at top:108 (third
     in the vertical chrome stack) — popover offsets by another
     button-height + gap (40 + 8 = 48) so they don't overlap. */
  top: 156px;
  right: 12px;
  z-index: 30;
  min-width: 296px;
  max-width: calc(100vw - 24px);
  background: rgba(26, 29, 39, 0.96);
  border: 1px solid var(--c-surface-border);
  border-radius: var(--radius-md);
  padding: 14px 16px;
  box-shadow: 0 10px 30px rgba(0, 0, 0, 0.45);
  backdrop-filter: blur(12px);
  -webkit-backdrop-filter: blur(12px);
  font-family: var(--f-body);
  color: var(--c-ink-primary);
  opacity: 0;
  transform: translateY(-6px);
  pointer-events: none;
  transition: opacity 160ms var(--ease-standard), transform 160ms var(--ease-standard);
}
.settings-popover[data-open="true"] {
  opacity: 1;
  transform: translateY(0);
  pointer-events: auto;
}
.settings-popover-title {
  font-family: var(--f-display);
  font-weight: 700;
  font-size: 11px;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--c-ink-secondary);
  margin-bottom: 10px;
}
/* === Wave 42 — Consolidated gear panel ============================
   Per-exercise client overrides for mute / treatment / body focus.
   Each control gets a coral border/tint when its value differs from
   the practitioner's per-exercise default. The Reset button is greyed
   when no overrides exist, coral + tappable otherwise. */

.settings-row-label {
  font-size: 14px;
  font-weight: 600;
  line-height: 1.25;
  color: var(--c-ink-primary);
}

.settings-panel-rows {
  display: flex;
  flex-direction: column;
  gap: 6px;
}

.settings-row-btn {
  appearance: none;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  padding: 10px 12px;
  min-height: 44px;
  background: rgba(255, 255, 255, 0.04);
  border: 1.5px solid transparent;
  border-radius: 8px;
  color: var(--c-ink-primary);
  font-family: var(--f-body);
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
  transition: background 140ms var(--ease-standard), border-color 140ms var(--ease-standard), color 140ms var(--ease-standard);
}
.settings-row-btn:hover:not(:disabled) {
  background: rgba(255, 255, 255, 0.08);
}
.settings-row-btn:focus-visible {
  outline: 2px solid var(--c-brand);
  outline-offset: 2px;
}
.settings-row-btn .settings-row-state {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 24px;
  height: 24px;
  color: var(--c-ink-secondary);
  flex: 0 0 auto;
}
.settings-row-btn .settings-row-state svg {
  width: 22px;
  height: 22px;
  display: block;
  stroke: currentColor;
}

/* Overridden state — coral border + coral icon/text tint. Use the
   brand variable; #FF6B35 is the literal hex for reference. */
.settings-row-btn.is-overridden {
  border-color: var(--c-brand);
  color: var(--c-brand);
}
.settings-row-btn.is-overridden .settings-row-label,
.settings-row-btn.is-overridden .settings-row-state {
  color: var(--c-brand);
}

.settings-row-btn:disabled,
.settings-row-btn.is-disabled {
  cursor: not-allowed;
  opacity: 0.45;
}

/* Treatment row — label + 3-pill segmented control. */
.settings-row-segmented {
  display: flex;
  flex-direction: column;
  gap: 8px;
  padding: 10px 12px;
  background: rgba(255, 255, 255, 0.04);
  border: 1.5px solid transparent;
  border-radius: 8px;
  transition: border-color 140ms var(--ease-standard);
}
.settings-row-segmented.is-overridden {
  border-color: var(--c-brand);
}
.settings-row-segmented.is-overridden .settings-row-label {
  color: var(--c-brand);
}

.treatment-pills {
  display: grid;
  grid-template-columns: 1fr 1fr 1fr;
  gap: 4px;
  background: rgba(0, 0, 0, 0.3);
  border-radius: 6px;
  padding: 3px;
}
.treatment-pills > button {
  appearance: none;
  background: transparent;
  border: 0;
  border-radius: 4px;
  padding: 8px 4px;
  min-height: 36px;
  color: var(--c-ink-secondary);
  font-family: var(--f-body);
  font-size: 13px;
  font-weight: 600;
  line-height: 1.15;
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
  transition: background 140ms var(--ease-standard), color 140ms var(--ease-standard);
  text-align: center;
}
.treatment-pills > button:hover:not(.is-disabled):not(.is-active) {
  background: rgba(255, 255, 255, 0.06);
  color: var(--c-ink-primary);
}
.treatment-pills > button:focus-visible {
  outline: 2px solid var(--c-brand);
  outline-offset: 2px;
}
.treatment-pills > button.is-active {
  background: rgba(255, 255, 255, 0.08);
  color: var(--c-ink-primary);
}
.treatment-pills > button.is-active.is-overridden {
  background: var(--c-brand); /* #FF6B35 */
  color: #FFFFFF;
}
.treatment-pills > button.is-disabled {
  cursor: not-allowed;
  color: rgba(255, 255, 255, 0.32);
}
.treatment-pills > button.is-disabled:hover {
  background: transparent;
}

/* Reset button. Greyed when no overrides; coral + tappable otherwise. */
.settings-row-reset {
  appearance: none;
  margin-top: 6px;
  padding: 10px 12px;
  min-height: 40px;
  background: transparent;
  border: 1px solid var(--c-brand);
  border-radius: 8px;
  color: var(--c-brand);
  font-family: var(--f-body);
  font-size: 13px;
  font-weight: 600;
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
  transition: background 140ms var(--ease-standard), opacity 140ms var(--ease-standard);
}
.settings-row-reset:hover:not(:disabled):not(.is-empty) {
  background: rgba(255, 107, 53, 0.08);
}
.settings-row-reset:focus-visible {
  outline: 2px solid var(--c-brand);
  outline-offset: 2px;
}
.settings-row-reset.is-empty,
.settings-row-reset:disabled {
  border-color: var(--c-surface-border);
  color: var(--c-ink-secondary);
  opacity: 0.5;
  cursor: not-allowed;
}

/* === Edge chevron nav (overlaid on video) ========================= */
.edge-nav {
  position: absolute;
  top: 50%;
  transform: translateY(-50%);
  width: 52px;
  height: 72px;
  border: none;
  background: transparent;
  color: var(--c-brand);
  display: flex;
  align-items: center;
  justify-content: center;
  cursor: pointer;
  z-index: 15;
  transition: opacity 180ms ease, transform 100ms ease, background 150ms ease;
  -webkit-tap-highlight-color: transparent;
  padding: 0;
  border-radius: 12px;
}
.edge-nav:hover {
  background: rgba(0, 0, 0, 0.28);
}
.edge-nav:active {
  transform: translateY(-50%) scale(0.94);
}
.edge-nav svg {
  stroke: currentColor;
  filter: drop-shadow(0 2px 6px rgba(0, 0, 0, 0.6));
}
.edge-nav:disabled {
  opacity: 0.25;
  cursor: not-allowed;
}
.edge-nav:focus-visible {
  outline: 2px solid var(--c-brand);
  outline-offset: 2px;
}
.edge-nav--prev { left: 6px; }
.edge-nav--next { right: 6px; }

/* Carl 2026-04-24: when the vertical rep stack is rendered on the left
   edge, the prev chevron sat dead-centre over it. Shift the chevron
   just past the stack column so both stay tappable. JS adds the
   `has-rep-stack` class on `.card-viewport` whenever the stack is
   visible (more reliable than :has() across iOS Safari versions). */
.card-viewport.has-rep-stack .edge-nav--prev {
  left: 64px;
}

/* Placeholder when no media */
.media-placeholder {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 12px;
  padding: 32px;
  color: var(--c-ink-secondary);
}

.media-placeholder-icon {
  width: 64px;
  height: 64px;
  border: 2px dashed var(--c-surface-border);
  border-radius: var(--radius-md);
  display: flex;
  align-items: center;
  justify-content: center;
}

.media-placeholder-icon svg {
  width: 28px;
  height: 28px;
  stroke: var(--c-ink-secondary);
}

.media-placeholder-text {
  font-size: 13px;
  color: var(--c-ink-secondary);
}

/* Video play overlay (kept for the rare case where video is shown
   outside workout mode — not used in the current flow but harmless). */
.video-play-overlay {
  position: absolute;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  background: rgba(0,0,0,0.15);
  cursor: pointer;
  transition: background var(--dur-fast) var(--ease-standard);
}

.video-play-overlay:active {
  background: rgba(0,0,0,0.30);
}

.play-button {
  width: 56px;
  height: 56px;
  background: var(--c-brand);
  border-radius: 50%;
  display: flex;
  align-items: center;
  justify-content: center;
  transition: transform var(--dur-fast) var(--ease-standard);
}

.video-play-overlay:active .play-button {
  transform: scale(0.92);
}

.play-button svg {
  width: 22px;
  height: 22px;
  margin-left: 3px;
  fill: #ffffff;
}

.video-play-overlay.is-hidden {
  opacity: 0;
  pointer-events: none;
}

/* === B&W treatment — CSS grayscale filter =========================
   The practitioner may prefer the B&W treatment; backend serves the
   original video / photo at grayscale_url and we filter client-side.
   Wave 22 extended the rule to <img> so photos get the same
   single-source three-treatment behaviour as videos (the raw colour
   JPG is loaded once; B&W and Colour share the same src and only
   the .is-grayscale class differs). */
video.is-grayscale,
img.is-grayscale {
  filter: grayscale(1) contrast(1.05);
}

/* === Video poster state — shown during prep (no playback yet) ===
   When the video is paused + currentTime=0 we still want it to read
   as "standing by", not "broken frame". The video element shows its
   poster naturally; this class is a marker for app.js to avoid
   accidental plays. */
video.is-prep-paused {
  /* No visual; semantic marker only. */
}

/* === Card notes overlay — centered soft-card (Wave 19 variant A) ====
   Centered horizontally above the bottom edge, rendered as a dark
   blurred pill with a subtle coral border. Off-white text for
   readability; coral reserved for accent budget. 3-line clamp by
   default; clicking expands to full. */
.card-notes-overlay {
  position: absolute;
  left: 50%;
  bottom: 12%;
  transform: translateX(-50%);
  z-index: 12;
  text-align: left;
  background: rgba(15, 17, 23, 0.78);
  backdrop-filter: blur(8px);
  -webkit-backdrop-filter: blur(8px);
  border: 1px solid rgba(255, 107, 53, 0.35);
  border-radius: 10px;
  padding: 12px 16px;
  max-width: 72%;
  min-width: 180px;
  color: #F5F5F7;
  cursor: pointer;
  font-family: var(--f-body);
  font-size: 14px;
  font-weight: 500;
  line-height: 1.4;
  letter-spacing: 0.1px;
  -webkit-tap-highlight-color: transparent;
  transition: opacity 180ms ease, border-color 180ms ease;
}
.card-notes-overlay .card-notes-text {
  display: block;
  display: -webkit-box;
  -webkit-line-clamp: 3;
  -webkit-box-orient: vertical;
  overflow: hidden;
  word-break: break-word;
}
.card-notes-overlay.is-expanded .card-notes-text {
  display: block;
  -webkit-line-clamp: unset;
  overflow: visible;
}
.card-notes-overlay:focus-visible {
  outline: 2px solid var(--c-brand);
  outline-offset: 2px;
}

/* === Rest countdown overlay ======================================
   Big coral countdown number over the last visible frame during a
   rest slide. Reuses the .prep-overlay sizing + style. */
.rest-countdown-overlay {
  position: absolute;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  pointer-events: none;
  z-index: 11;
  color: var(--c-brand);
  background: rgba(0, 0, 0, 0.32);
}
.rest-countdown-overlay[hidden] { display: none; }
.rest-countdown-number {
  font-family: var(--f-display);
  font-size: 180px;
  font-weight: 800;
  line-height: 1;
  letter-spacing: -8px;
  text-shadow: 0 6px 28px rgba(0, 0, 0, 0.55);
}

/* === Wave 21 — Vertical rep-block stack ============================
   Replaces the Wave 19.7 horizontal segmented bar. Pinned to the
   LEFT edge of .card-viewport, vertically centered, ~80% video-
   height tall. Bottom-up stacking metaphor:
     Set 1 (bottom)
       → reps fill upward
       → trailing rest block
     Set 2
       → reps fill upward
       → trailing rest block
     …
     Set N
       → reps fill upward
       → TRAILING rest block (the "you're done, breathe" cue before
         the slide advances — owned by advanceSetPhase()).

   Two flex children:
     .rep-stack-labels — left gutter, section labels (S1, R, S2, …)
                         vertically centered against each section.
     .rep-stack-column — the block column itself; one micro-block
                         per rep + one thinner block per rest.

   Rep blocks: empty = surface-raised + 1px coral-tint border,
               filled = solid coral, 200ms ease-in fill. Active
               (next-to-land) gets a 1Hz coral pulse on the outline.
   Rest blocks: 70% the height of a rep block (eye reads "different
                category" with no label). Empty = surface-raised +
                sage-tint border. Filling = solid sage, time-based
                bottom-up grow over the rest duration.

   Hidden when: rest slide, photo, single-set with no breather, or
   slide.reps null/0 (legacy). */

.rep-stack {
  position: absolute;
  left: 8px;
  top: 10%;
  bottom: 10%;
  display: flex;
  flex-direction: row;
  align-items: stretch;
  gap: 3px;
  /* Bug fix 2026-05-03 (C): width 50px → 34px (~32% reduction) +
     tightened gap so the rep stack stops colliding with the centered
     notes overlay. The aside weight chips still bleed slightly past
     this width but render with smaller padding (see .weight-chip
     below). */
  width: 34px;
  z-index: 12;
  /* Carl 2026-04-24: blocks need to receive taps for navigable stack
     (v52). The container itself stays pass-through so the gaps between
     blocks don't intercept video pause/play taps; .rep-stack-block
     re-enables pointer-events on the actual targets. */
  pointer-events: none;
}
.rep-stack-block {
  pointer-events: auto;
}
.rep-stack[hidden] { display: none; }

.rep-stack-labels {
  position: relative;
  /* Bug fix 2026-05-03 (C): 18px → 13px (~28% reduction) to match
     the parent stack tightening. Single-character labels (S1/S2/R)
     still render legibly at this width. */
  width: 13px;
  flex: 0 0 13px;
  display: flex;
  flex-direction: column-reverse; /* section 1 sits at the bottom */
}
.rep-stack-labels-section {
  position: relative;
  display: flex;
  align-items: center;
  justify-content: flex-end;
  padding-right: 4px;
  font-family: "JetBrains Mono", ui-monospace, monospace;
  font-size: 9px;
  font-weight: 700;
  letter-spacing: 0.3px;
  text-shadow: 0 1px 2px rgba(0, 0, 0, 0.7);
}
.rep-stack-labels-section--set { color: var(--c-brand); }
.rep-stack-labels-section--rest { color: var(--c-rest); }
.rep-stack-labels-section::after {
  /* Hairline bracket connecting the label to its section's
     vertical extent. Subtle — just a 1px right edge that runs
     the full height of the section. */
  content: "";
  position: absolute;
  right: 0;
  top: 4px;
  bottom: 4px;
  width: 1px;
  background: currentColor;
  opacity: 0.35;
}

.rep-stack-column {
  position: relative;
  flex: 1 1 auto;
  display: flex;
  flex-direction: column-reverse; /* set 1 + first rep at the bottom */
  gap: 1px;
}

/*
 * Wave 41 — sections become a horizontal split: rep-blocks column on
 * the LEFT, weight-chip / hold-label column on the RIGHT. The blocks
 * subcolumn keeps the original column-reverse so rep 1 sits at the
 * bottom; the aside subcolumn is centered next to its set group.
 */
.rep-stack-section {
  display: flex;
  flex-direction: row;
  align-items: center;
  gap: 6px;
  flex: var(--rep-stack-section-grow, 1) 1 0;
  min-height: 0;
  /* 2026-05-03: positioning context for the aside chip which
     absolutely-positions to the right of the column so a wide
     "120 kg · 12 s" pill doesn't squash the rep-blocks column to 0
     inside a 34px rep-stack. */
  position: relative;
}
.rep-stack-section--rest {
  /* Rest sections render as a single thinner block — already a
     single child below; the section just provides the slot. */
}

.rep-stack-section-blocks {
  display: flex;
  flex-direction: column-reverse;
  gap: 1px;
  flex: 1 1 auto;
  min-width: 0;
  min-height: 0;
  align-self: stretch;
}

.rep-stack-section-aside {
  /* 2026-05-03: anchored to the right edge of the section, OUTSIDE the
     rep-stack column. This frees the blocks column to use the full
     34px width and lets the chip render at its natural size in the
     card-viewport gutter beside the stack.
     2026-05-04: gutter narrowed from natural-width (used to grow with
     "120 kg · 12 s" horizontal text) to a slim 22-px vertical strip,
     because the chip inside now rotates to book-spine orientation. */
  position: absolute;
  left: calc(100% + 4px);
  top: 0;
  bottom: 0;
  width: 22px;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 4px;
  /* Stay out of the rep-block tap zone — the aside is non-interactive
     visual annotation. */
  pointer-events: none;
}

/* ------------------------------------------------------------------ */
/*  Vertical (book-spine) weight chip inside the rep-stack aside       */
/* ------------------------------------------------------------------ */
/* 2026-05-04: when the chip sits in a `.rep-stack-section-aside` it
   rotates 90° anticlockwise so the text reads bottom-to-top alongside
   the rep blocks. Same pattern as the mobile _MediaViewer treatment
   pill. The horizontal min-width / min-height swap so the chip's
   slim width matches the aside gutter and its height grows with the
   section it sits beside (the parent section's grow weight already
   scales the aside). (End-of-exercise badge retired 2026-05-04 —
   final rest section now renders no aside element at all.) */
.rep-stack-section-aside .weight-chip {
  writing-mode: vertical-rl;
  transform: rotate(180deg);
  /* Slim vertical chip: width is fixed, height grows to fit content
     with a min-height tall enough that a single-digit "5 kg" chip
     doesn't visually collapse next to a "120 kg · 12 s" chip. */
  min-width: 0;
  min-height: 56px;
  width: 18px;
  padding: 6px 2px;
  /* Letter-spacing in vertical writing-mode reads as inter-character
     spacing along the text flow direction (i.e. vertical) — keep
     the existing 0.3px so glyphs don't crowd each other. */
}
/* The bolt glyph is naturally rotated by the parent transform — its
   margin-right (horizontal-mode) becomes a margin in the vertical
   axis after the rotation. Tighten so the bolt sits flush against
   the digits rather than floating above them. */
.rep-stack-section-aside .weight-chip-bolt {
  margin-right: 0;
}

.rep-stack-set-hold {
  font-family: var(--f-mono);
  font-size: 9px;
  font-weight: 600;
  letter-spacing: 0.3px;
  color: var(--c-ink-muted);
  text-shadow: 0 1px 2px rgba(0, 0, 0, 0.7);
}

/* ------------------------------------------------------------------ */
/*  Weight chips (Wave 41; End-of-exercise marker retired 2026-05-04) */
/* ------------------------------------------------------------------ */
.weight-chip {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 3px;
  /* 2026-05-03 (rep-stack-rescue v2): aside is now absolutely-
     positioned outside the 34px rep-stack column, so chip min-width
     can be restored without collapsing the sibling blocks column.
     Bug-D's pixel uniformity reinstated. */
  padding: 2px 6px;
  min-width: 68px;
  min-height: 18px;
  box-sizing: border-box;
  text-align: center;
  border-radius: 999px;
  background: var(--c-surface-raised);
  border: 1px solid var(--c-surface-border);
  font-family: var(--f-mono);
  font-size: 9px;
  font-weight: 700;
  color: var(--c-ink-primary);
  letter-spacing: 0.3px;
  white-space: nowrap;
}

/* Urgent variant — used for the FIRST set's chip and any set whose
   weight differs from the previous one. Brand-tint background +
   coral text mirrors the mockup's "weight-up" treatment. */
.weight-chip--up {
  background: var(--c-brand-tint-bg);
  border-color: var(--c-brand-tint-border);
  color: var(--c-brand);
}

/* Bodyweight variant — muted styling with no urgent treatment so it
   reads as "no weight selected" rather than "change incoming". */
.weight-chip--bodyweight {
  background: transparent;
  border-color: var(--c-ink-disabled);
  color: var(--c-ink-secondary);
  font-weight: 500;
  letter-spacing: 0.4px;
}

.weight-chip-bolt {
  display: inline-block;
  margin-right: 1px;
  font-size: 11px;
  line-height: 1;
  color: var(--c-brand);
}
.weight-chip--bodyweight .weight-chip-bolt {
  color: var(--c-ink-muted);
}

/* End-of-exercise marker retired 2026-05-04 (Carl: "serves no
   purpose"). All rules removed; final rest section now renders no
   aside element at all. */

.rep-stack-block {
  position: relative;
  width: 100%;
  flex: 1 1 0;
  min-height: 2px;
  border-radius: 2px;
  overflow: hidden;
  background: var(--c-surface-raised);
  box-shadow: inset 0 0 0 1px var(--c-brand-tint-border);
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
}
.rep-stack-block:active {
  transform: scale(0.96);
  transition: transform 80ms ease-out;
}
.rep-stack-block--rest {
  /* Carl 2026-04-24: rest block now matches rep-block sizing so the
     column reads as a uniform stack — colour (sage vs coral) carries
     the "different category" signal, not size. The rest section is
     given the same flex grow as ONE rep block (see the inline style
     in the JS structure builder), so a 10-rep set has 11 equally
     sized blocks (10 coral reps + 1 sage rest). */
  background: var(--c-surface-raised);
  box-shadow: inset 0 0 0 1px var(--c-rest-tint-border);
  margin-top: 2px;
}

/* Filled rep block — solid coral, animated via the fill child so the
   200ms ease-in lands smoothly on each loop seam. */
.rep-stack-block-fill {
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  height: 0%;
  background: var(--c-brand);
  transition: height 200ms ease-in;
  will-change: height;
}
.rep-stack-block--filled .rep-stack-block-fill {
  height: 100%;
}

/* Carl 2026-04-24: active rep block doubles as a per-rep progress
   bar. Linear 1.05s transition matches the 1Hz repaint cadence, with
   a tiny tail so the fill never visually stalls between ticks. */
.rep-stack-block--active .rep-stack-block-fill {
  transition: height 1.05s linear;
}

/* Drain animation when the slide changes — every fill shrinks to 0
   over 350ms before the column rebuilds for the new slide. Set via
   the .is-draining class on the column root. */
.rep-stack-column.is-draining .rep-stack-block-fill {
  height: 0% !important;
  transition: height 350ms ease-out;
}
.rep-stack-block--rest .rep-stack-block-fill {
  background: var(--c-rest);
  /* Rest fill is time-based (linear over the rest duration); the
     200ms ease-in default is overridden inline via element style. */
  transition: height 0.95s linear;
}

/* Active rep block — the next one to land. Subtle 1Hz coral pulse on
   the outline so the eye tracks "you're about to fill here". */
.rep-stack-block--active {
  animation: rep-stack-active-pulse 1s ease-in-out infinite;
}
@keyframes rep-stack-active-pulse {
  0%, 100% {
    box-shadow: inset 0 0 0 1px var(--c-brand-tint-border);
  }
  50% {
    box-shadow: inset 0 0 0 1.5px var(--c-brand),
                0 0 6px rgba(255, 107, 53, 0.45);
  }
}

/* Active rest block — sage variant of the same pulse so the rest
   countdown reads as "currently filling" without text. */
.rep-stack-block--rest.rep-stack-block--active {
  animation: rep-stack-active-rest-pulse 1s ease-in-out infinite;
}
@keyframes rep-stack-active-rest-pulse {
  0%, 100% {
    box-shadow: inset 0 0 0 1px var(--c-rest-tint-border);
  }
  50% {
    box-shadow: inset 0 0 0 1.5px var(--c-rest),
                0 0 6px rgba(134, 239, 172, 0.45);
  }
}

/* === Breather overlay =============================================== */
.breather-overlay {
  position: absolute;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  pointer-events: none;
  z-index: 11;
  color: var(--c-rest);
  background: rgba(0, 0, 0, 0.42);
  animation: breather-fade-in 220ms ease-out;
}
.breather-overlay[hidden] { display: none; }

.breather-overlay-inner {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 10px;
  text-shadow: 0 6px 28px rgba(0, 0, 0, 0.55);
}

.breather-overlay-glyph {
  width: 68px;
  height: 68px;
  color: var(--c-rest);
  opacity: 0.9;
  filter: drop-shadow(0 3px 10px rgba(0, 0, 0, 0.5));
}

.breather-overlay-number {
  font-family: var(--f-display);
  font-size: 160px;
  font-weight: 800;
  line-height: 1;
  letter-spacing: -7px;
  color: var(--c-rest);
}

.breather-overlay-label {
  font-family: var(--f-body);
  font-size: 13px;
  font-weight: 600;
  letter-spacing: 3px;
  text-transform: uppercase;
  color: var(--c-rest);
  opacity: 0.85;
}

@keyframes breather-fade-in {
  from { opacity: 0; }
  to   { opacity: 1; }
}

@media (max-width: 420px) {
  .breather-overlay-number { font-size: 120px; letter-spacing: -5px; }
  .breather-overlay-glyph  { width: 54px; height: 54px; }
}

/* === Rest card — inherit the surface gradient so the rest slide
     still has a calm sage pane behind the countdown. =============== */
.rest-card {
  background: linear-gradient(160deg, var(--c-surface-base) 0%, var(--c-surface-raised) 100%);
  border-color: var(--c-surface-border);
}

.card-media.rest-media {
  background: linear-gradient(160deg, var(--c-surface-base) 0%, var(--c-surface-raised) 100%);
}

.rest-display {
  flex: 1;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 12px;
  /* 2026-05-03 — rest card tightened: top + bottom padding reduced
     by 66% (32px → 11px) per Carl. Sides unchanged at 32px. */
  padding: 11px 32px;
  text-align: center;
}

.rest-icon {
  width: 80px;
  height: 80px;
  color: var(--c-rest);
  opacity: 0.7;
}

.rest-icon svg {
  width: 100%;
  height: 100%;
}

.rest-title {
  font-family: var(--f-display);
  font-size: 28px;
  font-weight: 700;
  color: var(--c-ink-primary);
  letter-spacing: -0.02em;
}

.rest-next-up {
  font-size: 14px;
  color: var(--c-ink-secondary);
  font-weight: 500;
  margin-top: 8px;
}

/* === Footer ====================================================== */
.footer {
  flex-shrink: 0;
  text-align: center;
  padding: 6px 20px 10px;
  background: var(--c-surface-bg);
}

.footer-brand {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 2px;
}

.footer-preamble {
  font-size: 10px;
  color: var(--c-ink-secondary);
  letter-spacing: 0.4px;
  text-transform: lowercase;
  opacity: 0.75;
}

.footer-logo {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  color: var(--c-brand);
}

.footer-logo .pulse-mark {
  width: 20px;
  height: 14px;
}

.footer-wordmark {
  font-family: var(--f-display);
  font-weight: 600;
  font-size: 12px;
  letter-spacing: -0.2px;
  color: var(--c-ink-secondary);
}

/* Discreet build marker — visible but not loud. Mirrors the mobile
   pattern (build SHA at 35% opacity in the HomefitLogo footer on Home).
   Sits beneath the wordmark; mono font so QA can read it at a glance. */
.footer-version {
  margin-top: 2px;
  font-family: var(--f-mono);
  font-size: 9px;
  letter-spacing: 0.2px;
  color: var(--c-ink-muted);
  opacity: 0.55;
  user-select: text;
}

/* === Workout Timer Mode ========================================== */

.start-workout-btn {
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 16px 32px;
  background: var(--c-brand);
  color: #ffffff;
  border: none;
  border-radius: 100px;
  font-family: var(--f-body);
  font-size: 17px;
  font-weight: 700;
  letter-spacing: 0.02em;
  cursor: pointer;
  z-index: 25;
  transition: all var(--dur-fast) var(--ease-standard);
  -webkit-tap-highlight-color: transparent;
  box-shadow: 0 10px 32px rgba(255, 107, 53, 0.45);
}

.start-workout-btn:hover {
  background: var(--c-brand-dark);
}

.start-workout-btn:active {
  transform: translate(-50%, -50%) scale(0.95);
}

.start-workout-btn svg {
  width: 22px;
  height: 22px;
  stroke: #ffffff;
}

/* Timer chip — legacy stub kept hidden; the top-row ETA now shows
   the same numbers the chip used to. */
.timer-overlay { display: none !important; }

/* === Workout complete overlay ==================================== */
.workout-complete {
  position: fixed;
  inset: 0;
  z-index: 50;
  display: flex;
  align-items: center;
  justify-content: center;
  background: rgba(0,0,0,0.45);
  backdrop-filter: blur(4px);
  -webkit-backdrop-filter: blur(4px);
  pointer-events: auto;
}

.workout-complete-inner {
  position: relative;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 16px;
  padding: 40px 40px 32px;
  background: transparent;
  border: none;
  text-align: center;
  max-width: 320px;
}

.workout-complete-glow {
  position: absolute;
  top: 20px;
  left: 50%;
  transform: translateX(-50%);
  width: 200px;
  height: 200px;
  border-radius: 50%;
  background: radial-gradient(
    circle,
    rgba(255, 107, 53, 0.22) 0%,
    rgba(255, 107, 53, 0.06) 45%,
    rgba(255, 107, 53, 0) 70%
  );
  pointer-events: none;
  opacity: 0;
}

.workout-complete.is-live .workout-complete-glow {
  animation: complete-glow 2s ease-out forwards;
}

@keyframes complete-glow {
  0%   { opacity: 0; transform: translateX(-50%) scale(0.75); }
  40%  { opacity: 1; }
  100% { opacity: 0.75; transform: translateX(-50%) scale(1); }
}

.workout-complete-icon {
  position: relative;
  width: 72px;
  height: 72px;
  color: var(--c-brand);
}

.workout-complete.is-live .workout-complete-icon {
  animation: complete-check 200ms var(--ease-emphasized) both;
}

@keyframes complete-check {
  0%   { transform: scale(0.6); opacity: 0; }
  100% { transform: scale(1); opacity: 1; }
}

.workout-complete-icon svg {
  width: 100%;
  height: 100%;
}

.workout-complete-title {
  position: relative;
  font-family: var(--f-display);
  font-size: 26px;
  font-weight: 700;
  color: var(--c-ink-primary);
  letter-spacing: -0.02em;
}

.workout-complete-time {
  position: relative;
  font-size: 16px;
  color: var(--c-ink-secondary);
  font-weight: 500;
  font-variant-numeric: tabular-nums;
}

.workout-close-btn {
  position: relative;
  margin-top: 8px;
  padding: 12px 32px;
  background: var(--c-brand);
  color: #ffffff;
  border: none;
  border-radius: 100px;
  font-family: var(--f-body);
  font-size: 15px;
  font-weight: 700;
  cursor: pointer;
  transition: all var(--dur-fast) var(--ease-standard);
  -webkit-tap-highlight-color: transparent;
}

.workout-close-btn:hover {
  background: var(--c-brand-dark);
}

.workout-close-btn:active {
  transform: scale(0.97);
}

/* === Prep countdown overlay ====================================== */
.prep-overlay {
  position: absolute;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  pointer-events: none;
  z-index: 11;
  color: var(--c-brand);
  background: rgba(0, 0, 0, 0.28);
}
.prep-overlay[hidden] { display: none; }
.prep-overlay-number {
  font-family: var(--f-display);
  font-size: 180px;
  font-weight: 800;
  line-height: 1;
  letter-spacing: -8px;
  text-shadow: 0 6px 28px rgba(0, 0, 0, 0.55);
  opacity: 1;
  transition: opacity 200ms var(--ease-standard);
  /* Stack above the Hero poster so the digit is always legible. */
  position: relative;
  z-index: 1;
}
.prep-overlay-number.is-fading { opacity: 0; }

/* Wave Hero — practitioner-picked Hero still surfaced during prep.
   Sits behind the countdown digit, full-bleed, object-fit:cover so it
   matches the framed media area exactly. A subtle dim (rgba black)
   above keeps the coral digit readable on bright frames. */
.hero-poster {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  z-index: 0;
  /* Soft dim so the coral countdown reads against bright Hero frames. */
  filter: brightness(0.62);
}

/* Smaller digit on narrow viewports. */
@media (max-width: 420px) {
  .prep-overlay-number,
  .rest-countdown-number { font-size: 140px; letter-spacing: -6px; }
}

/* === Flashing token ==============================================
   During prep, the ETA's current-slide number + the active pill
   pulse in sync. */
@keyframes prep-flash {
  0%, 100% { opacity: 1; }
  50%      { opacity: 0.4; }
}
.pill.is-active.is-prep-flashing {
  animation: prep-flash 600ms ease-in-out infinite;
}
.pill.is-active.is-prep-flashing { animation-name: prep-flash; }

/* === Homefit logo ================================================ */
.homefit-logo {
  width: 64px;
  height: 28px;
  display: block;
  color: var(--c-brand);
}
.homefit-logo-wordmark {
  font-family: var(--f-display);
  font-weight: 600;
  font-size: 13px;
  letter-spacing: -0.2px;
  color: var(--c-ink-secondary);
}

/* === Fullscreen / ambient mode ===================================
   When the viewport enters fullscreen (document.fullscreenElement is
   the root html element), JS adds `is-fullscreen` to <body>. In that
   state:
     • Only the plan-bar (session title + client name) and the footer
       hide entirely. The scoreboard — workout-timeline-bar,
       progress-matrix, and single-line active-slide-title — stays
       visible so the client can glance at circuit position + remaining
       time during the workout.
     • Every coral overlay drops to ~30% alpha (mute, fullscreen,
       chevrons, notes, pause disc) — ambient, not attention-hungry.
     • Adding `chrome-visible` on a brief tap pops them to 100% alpha
       for 3 seconds. See handleAmbientReveal() in app.js.
     • The fullscreen toggle itself stays at 100% alpha throughout
       (you need a way out).
*/
/* Defensive scroll lock for faux-fullscreen fallback on iPhone Safari
   (where Element.requestFullscreen is undefined). app.js also toggles the
   inline style on <html>/<body> for the faux path; this rule backs that
   up in case a future refactor forgets, and kills rubber-band scroll. */
body.is-fullscreen {
  overflow: hidden;
  overscroll-behavior: none;
}
body.is-fullscreen #app {
  /* Keep the status-bar / Dynamic Island inset on iPhone so the matrix
     pills + active-pill clock stop below the island instead of slipping
     under it. Phones without an island report 0 here, so this is a
     no-op on hardware that doesn't need the clamp. */
  padding-top: var(--safe-top);
  padding-bottom: 0;
}
body.is-fullscreen .plan-bar {
  display: none !important;
}

/* Fullscreen: keep the "powered by homefit.studio" mark visible but
   overlay it on the video bottom rather than reserving layout space. */
body.is-fullscreen .footer {
  position: fixed;
  left: 0;
  right: 0;
  bottom: 0;
  z-index: 20;
  pointer-events: none;
  /* Subtle gradient scrim so the wordmark is legible over bright video
     content without leaning on a hard bar. Darkens only the bottom 80px. */
  background: linear-gradient(to top, rgba(0, 0, 0, 0.65) 0%, rgba(0, 0, 0, 0.35) 60%, transparent 100%);
  opacity: 0.85;
  transition: opacity 220ms var(--ease-standard);
}
body.is-fullscreen.chrome-visible .footer {
  opacity: 1;
}

/* Fullscreen: drop the 32px max-width cap AND weight each pill by its
   estimated duration so the matrix reads as a proportional Gantt strip —
   a 60s exercise is twice as wide as a 30s rest. `--pill-weight` is set
   inline on each .matrix-col by buildProgressMatrix(); circuit blocks
   use `--circuit-weight` = Σ exercise durations of one round. Outside
   fullscreen the existing `flex: 1 1 0` / `flex: var(--circuit-cols)`
   rules win so short plans still render as equal-width pills. */
body.is-fullscreen .matrix-col {
  flex: var(--pill-weight, 1) 1 0;
  min-width: 12px;
  max-width: none;
}
body.is-fullscreen .matrix-circuit {
  flex: var(--circuit-weight, var(--circuit-cols, 1)) 1 0;
  min-width: calc(12px * var(--circuit-cols, 1) + 4px * (var(--circuit-cols, 1) - 1));
  max-width: none;
}
/* Fullscreen also weights pills WITHIN a circuit row by duration. --row-
   template-fs is an inline custom property set per-row by app.js (e.g.
   "30fr 90fr 45fr"). Non-fullscreen keeps the default equal-fr template. */
body.is-fullscreen .matrix-circuit-row {
  grid-template-columns: var(--row-template-fs);
}

/* Ambient-fullscreen chrome dimming retired (Carl 2026-04-24).
   The chrome buttons sit on a translucent dark background already —
   fading them further to 30% made them feel like they were greyed-
   out controls. They now stay at 100% opacity in all states; the
   dark-pill background is enough to keep them subtle. */

/* Popover stays 100% alpha while open — if the user opened it in
   ambient fullscreen, we don't want to fade their UI out from under them. */
body.is-fullscreen .settings-popover[data-open="true"] {
  opacity: 1;
}

/* Disabled prev stays at ambient 15% in fullscreen even on reveal —
   no sense lighting up something that does nothing. */
body.is-fullscreen .edge-nav:disabled {
  opacity: 0.15;
}

/* Fullscreen + playpause toggles are always 100% alpha so the user
   can always exit fullscreen / pause the workout. */
body.is-fullscreen .fullscreen-toggle,
body.is-fullscreen .playpause-toggle {
  opacity: 1;
}

/* === Responsive adjustments ====================================== */
@media (min-width: 480px) {
  .plan-bar {
    padding-left: 32px;
    padding-right: 32px;
  }
  .workout-timeline-bar {
    padding-left: 32px;
    padding-right: 32px;
  }
  .active-slide-header {
    padding-left: 32px;
    padding-right: 32px;
  }
}

/* Desktop: constrain to phone-like width */
@media (min-width: 640px) {
  body {
    display: flex;
    align-items: center;
    justify-content: center;
    background: #05060A;
  }

  #app {
    max-width: 430px;
    width: 100%;
    height: 100%;
    max-height: 932px;
    background: var(--c-surface-bg);
    border: 1px solid var(--c-surface-border);
    border-radius: var(--radius-xl);
    overflow: hidden;
    position: relative;
  }

  .loading-screen,
  .error-screen {
    border-radius: var(--radius-xl);
  }

  /* Keep workout overlays inside the app container on desktop */
  .workout-complete {
    position: absolute;
  }

  /* Fullscreen on desktop: drop the phone frame + fill the viewport. */
  body.is-fullscreen #app {
    max-width: none;
    max-height: none;
    border-radius: 0;
    border: 0;
  }
}

/* === Landscape orientation (Wave 28) ==============================
   Slim every horizontal row that lives above the card viewport so the
   hero region keeps its share of the canvas, and reflow the right-edge
   chrome stack to suit the wider, shorter shape. The portrait branch
   above stays untouched — every rule below only applies when the
   visual viewport is wider than it is tall.

   Keeps the desktop phone-frame (≥640px portrait) intact: the
   `(max-aspect-ratio: 1/1)` + (orientation: landscape) intersection
   below targets phones rotated landscape. Desktop landscape browsers
   are wide-enough that the existing portrait styles still feel right
   inside the 430×932 frame.
*/
@media (orientation: landscape) and (max-height: 540px) {
  /* Slim plan-bar — ~28px tall vs the portrait 14+6 padding chunk. */
  .plan-bar {
    padding: 4px 16px 2px;
  }
  .plan-title {
    font-size: 15px;
    line-height: 1.15;
  }
  .header-client {
    font-size: 11px;
  }

  /* Active-slide header: compact pill above the matrix. Same content,
     tighter padding + smaller font so it shares vertical real estate. */
  .active-slide-header {
    padding: 2px 16px 4px;
  }
  .active-slide-title {
    font-size: 14px;
    line-height: 1.2;
  }

  /* Workout timeline strip: keep horizontal, halve vertical padding. */
  .workout-timeline-bar {
    padding: 1px 12px 2px;
    font-size: 10px;
  }
  .workout-timeline-total {
    font-size: 11px;
  }

  /* Progress matrix: shrink the inner padding + drop the bottom margin
     so the matrix hugs the timeline strip. The wider canvas means more
     pills fit per row visually; we let the existing flex-distribute
     logic do the layout, just give it a slimmer band. */
  .progress-matrix {
    margin: 1px 0 2px;
  }
  .progress-matrix-inner {
    padding: 1px 12px;
    gap: 6px;
  }
  /* Pills can be a touch shorter — visual cap mirrors the new tier. */
  .matrix-col {
    max-width: 40px;
  }

  /* Card viewport gets every spare pixel — no max-height, no padding. */
  .card-viewport {
    padding: 0;
  }

  /* Edge chevrons stay mid-vertical-edge; pull them in slightly so they
     don't compete with the right-edge chrome stack on the slimmer band. */
  .edge-nav {
    width: 44px;
    height: 60px;
  }
  .edge-nav--prev { left: 4px; }
  .edge-nav--next { right: 4px; }
  .card-viewport.has-rep-stack .edge-nav--prev {
    left: 56px;
  }

  /* Right-edge chrome: tighten the stack so it doesn't letterbox into
     the card area on the shorter canvas. Wave 42 retired the standalone
     mute speaker — gear panel is the only mute entry point now. */
  .playpause-toggle,
  .fullscreen-toggle,
  .settings-toggle {
    width: 36px;
    height: 36px;
  }
  .playpause-toggle {
    top: 8px;
    right: 8px;
  }
  .fullscreen-toggle {
    top: 50px;
    right: 8px;
  }
  .settings-toggle {
    top: 92px;
    right: 8px;
  }

  /* Settings popover: anchor as a popover (not a near-full-bottom-sheet).
     The wider canvas would otherwise leave the popover marooned in the
     lower-right corner. Keep its anchor under the gear button + cap the
     width so it sits as a proper floating card. */
  .settings-popover {
    top: 134px;
    right: 8px;
    min-width: 280px;
    max-width: 360px;
    padding: 10px 14px;
  }

  /* Body focus pill (mobile inline preview chrome) — kept from
     colliding with the mute pill on the slimmer canvas. The pill lives
     above the mute button at bottom-left in the mobile preview path;
     match the tighter mute coords. */
  .body-focus-pill {
    bottom: 56px;
    left: 8px;
  }

  /* Footer: thin band so the card viewport keeps its share. */
  .footer {
    padding: 2px 16px 4px;
  }
  .footer-version {
    font-size: 8px;
  }

  /* Maximise pill — landscape pre-workout only on the LIVE web surface.
     The body-class guards hide it once the workout starts (Start Workout
     already requests fullscreen), when fullscreen is already active, OR
     when the bundle is loaded inside the mobile-preview WebView (the
     pill is purely an iOS-Safari-chrome workaround for client viewers
     and has no business in practitioner-side preview). */
  body:not(.is-workout-mode):not(.is-fullscreen):not(.is-local-preview) .landscape-maximise-pill {
    display: inline-flex;
    position: fixed;
    top: 50%;
    right: 12px;
    transform: translateY(-50%);
    z-index: 50;
    align-items: center;
    gap: 8px;
    padding: 10px 14px;
    border: 1px solid rgba(255, 107, 53, 0.55);
    border-radius: 999px;
    background: rgba(0, 0, 0, 0.78);
    color: var(--c-brand);
    font-family: inherit;
    font-size: 13px;
    font-weight: 600;
    letter-spacing: 0.2px;
    cursor: pointer;
    -webkit-tap-highlight-color: transparent;
    box-shadow: 0 8px 20px rgba(0, 0, 0, 0.45);
    transition: background 150ms ease, transform 100ms ease;
  }
  body:not(.is-workout-mode):not(.is-fullscreen):not(.is-local-preview) .landscape-maximise-pill:hover {
    background: rgba(0, 0, 0, 0.92);
  }
  body:not(.is-workout-mode):not(.is-fullscreen):not(.is-local-preview) .landscape-maximise-pill:active {
    transform: translateY(-50%) scale(0.96);
  }
  body:not(.is-workout-mode):not(.is-fullscreen):not(.is-local-preview) .landscape-maximise-pill svg {
    width: 16px;
    height: 16px;
    display: block;
  }
}

/* Landscape exercise-card aspect-ratio handling (Wave 28).
   The card-viewport itself is always full-bleed; the per-exercise
   aspect ratio drives whether the source video fills the card or
   letterboxes inside it.

     • aspect-ratio > 1 (landscape source) — let object-fit: contain
       letterbox top/bottom on a portrait viewport, but on a landscape
       viewport the source already matches the canvas shape, so the
       contain rule effectively fills.

     • aspect-ratio < 1 (portrait source) — object-fit: contain
       letterboxes left/right on a landscape viewport. The pillarbox
       background is the same #0A0C12 that .card-media already paints.

   No new layout rules required: the existing .card-media + video /
   img object-fit: contain pair handles both cases correctly. The
   `--exercise-aspect-ratio` custom property is set on each card for
   future use (preload sizing, OG card generation) — readable via
   getComputedStyle without round-tripping through the video metadata
   event.
*/

/* ================================================================
   Wave 17 — Analytics consent banner
   ================================================================ */

.analytics-consent-banner {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  z-index: 9999;
  transform: translateY(-100%);
  transition: transform var(--dur-slow) var(--ease-emphasized);
  pointer-events: none;
}

.analytics-consent-banner.is-visible {
  transform: translateY(0);
  pointer-events: auto;
}

.analytics-consent-inner {
  background: var(--c-surface-raised);
  border-bottom: 2px solid var(--c-brand);
  padding: 20px 24px 16px;
  text-align: center;
  max-width: 480px;
  margin: 0 auto;
  border-radius: 0 0 var(--radius-md) var(--radius-md);
}

.analytics-consent-title {
  font-family: var(--f-display);
  font-weight: 700;
  font-size: 16px;
  color: var(--c-ink-primary);
  margin-bottom: 6px;
}

.analytics-consent-body {
  font-family: var(--f-body);
  font-size: 13px;
  color: var(--c-ink-secondary);
  line-height: 1.5;
  margin-bottom: 16px;
}

.analytics-consent-actions {
  display: flex;
  gap: 10px;
  justify-content: center;
  margin-bottom: 12px;
}

.analytics-consent-btn {
  font-family: var(--f-body);
  font-size: 13px;
  font-weight: 600;
  padding: 8px 20px;
  border-radius: var(--radius-sm);
  border: none;
  cursor: pointer;
  transition: all var(--dur-fast) var(--ease-standard);
}

.analytics-consent-decline {
  background: transparent;
  color: var(--c-ink-secondary);
  border: 1px solid var(--c-surface-border);
}

.analytics-consent-decline:hover {
  background: rgba(255, 255, 255, 0.05);
  border-color: var(--c-ink-muted);
}

.analytics-consent-accept {
  background: var(--c-brand);
  color: #fff;
}

.analytics-consent-accept:hover {
  background: var(--c-brand-dark);
}

.analytics-consent-link {
  display: inline-block;
  font-family: var(--f-body);
  font-size: 12px;
  color: var(--c-ink-muted);
  text-decoration: none;
  transition: color var(--dur-fast);
}

.analytics-consent-link:hover {
  color: var(--c-brand);
}

/* Wave 17 — Completion CTA */

.analytics-completion-cta {
  margin-top: 12px;
  margin-bottom: 4px;
}

.analytics-cta-link {
  display: inline-block;
  font-family: var(--f-body);
  font-size: 13px;
  font-weight: 600;
  color: var(--c-brand);
  text-decoration: none;
  padding: 8px 16px;
  border: 1px solid var(--c-brand-tint-border);
  border-radius: var(--radius-sm);
  background: var(--c-brand-tint-bg);
  transition: all var(--dur-fast) var(--ease-standard);
}

.analytics-cta-link:hover {
  background: rgba(255, 107, 53, 0.20);
  border-color: var(--c-brand);
}

/* ── Reduced motion a11y ─────────────────────────────────────── */
/* Disables all animations (pill pulse, prep flash, breather fade,
   completion glow, rep-stack pulse) for motion-sensitive users
   while preserving functional state changes. */
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
  }
}

/* ── Error retry button ──────────────────────────────────────── */
.error-retry-btn {
  display: inline-block;
  margin-top: 16px;
  padding: 10px 28px;
  font: var(--t-label-lg);
  color: var(--c-ink-primary);
  background: var(--c-brand);
  border: none;
  border-radius: var(--radius-sm);
  cursor: pointer;
  transition: background var(--dur-fast) var(--ease-standard);
}
.error-retry-btn:hover {
  background: var(--c-brand-dark);
}

/* ── Video refresh indicator (URL expiry retry) ──────────────── */
.video-refresh-indicator {
  position: absolute;
  bottom: 8px;
  left: 50%;
  transform: translateX(-50%);
  font: var(--t-label-sm);
  color: var(--c-ink-secondary);
  background: rgba(15, 17, 23, 0.75);
  padding: 4px 12px;
  border-radius: var(--radius-sm);
  pointer-events: none;
  z-index: 20;
}

/* ===========================================================================
   Lobby (PR 4/4 of the lobby train, 2026-05-04)
   =========================================================================== */

#lobby {
  position: fixed;
  inset: 0;
  display: flex;
  flex-direction: column;
  background: var(--c-surface-bg);
  color: var(--c-ink-primary);
  z-index: 50;
  overflow: hidden;
  padding-top: var(--safe-top);
}

.lobby-inner {
  flex: 1 1 auto;
  overflow-y: auto;
  -webkit-overflow-scrolling: touch;
  /* Padding-bottom = sticky CTA bar height (~140px) + safe-area + breath. */
  padding-bottom: calc(160px + var(--safe-bottom));
  max-width: 720px;
  width: 100%;
  margin: 0 auto;
  padding-left: 16px;
  padding-right: 16px;
}

/* Metadata strip ----------------------------------------------------------- */
.lobby-meta {
  padding: 24px 4px 16px;
}

.lobby-meta-headline {
  font-family: var(--f-body);
  font-size: 18px;
  font-weight: 600;
  line-height: 1.3;
  margin: 0 0 4px;
  color: var(--c-ink-primary);
  letter-spacing: -0.01em;
}

.lobby-meta-headline.is-custom-title {
  font-family: var(--f-display);
  font-size: 22px;
  font-weight: 700;
  letter-spacing: -0.02em;
}

.lobby-meta-sub {
  font-family: var(--f-body);
  font-size: 13px;
  font-weight: 400;
  line-height: 1.45;
  color: var(--c-ink-secondary);
  margin: 0;
}

.lobby-meta-stamp {
  display: inline-block;
  margin-top: 6px;
  font-family: var(--f-mono);
  font-size: 11px;
  color: var(--c-ink-muted);
  letter-spacing: 0.02em;
}

/* Hotfix round 3 — Fix D — build-marker chip relocated to top-left
   fixed corner. Surfaces plan version + bundle version + active SW
   cache label so QA can spot a stale browser shell at a glance. Pinned
   absolutely to #lobby (which is itself position: fixed, inset: 0).
   z-index 51 sits above .lobby-matrix's z-index 5 (sticky strip)
   without competing with #lobby's own z-index 50 surface. The chip is
   a sibling of .lobby-inner so it never participates in the meta
   layout's vertical flow. When Start is tapped #lobby is hidden, so
   the chip disappears with it — no extra hide logic needed.
   text-decoration: none + pointer-events: none keeps it visually
   present but inert (it's a marker, not a control). */
.lobby-meta-version {
  position: absolute;
  top: 6px;
  left: 8px;
  z-index: 51;
  font-family: var(--f-mono);
  font-size: 10px;
  color: var(--c-ink-muted);
  opacity: 0.55;
  letter-spacing: 0.02em;
  pointer-events: none;
  /* Respect notched-device safe area — push down by the device's safe
     top inset. #lobby already adds padding-top: var(--safe-top), so
     the chip would otherwise overlap the status bar on landscape iOS. */
  padding-top: var(--safe-top);
}

/* Pill matrix coupling ---------------------------------------------------- */
.lobby-matrix {
  position: sticky;
  top: 0;
  z-index: 5;
  background: linear-gradient(
    to bottom,
    var(--c-surface-bg) 0%,
    var(--c-surface-bg) 80%,
    transparent 100%
  );
  padding: 8px 0 12px;
  margin: 0 -16px;
}

.lobby-matrix-inner {
  display: flex;
  /* Wave 5 lobby fixes — wider gap so neighbour pills don't capture
     stray taps. Deck uses 4px (compact); lobby is the primary
     navigation surface and benefits from 6px. */
  gap: 6px;
  align-items: stretch;
  padding: 0 16px;
  /* When the matrix overflows the viewport (long plans), allow
     horizontal scroll so individual pills retain their min-width
     instead of being squashed below tap accuracy. */
  overflow-x: auto;
  scrollbar-width: none;
}
.lobby-matrix-inner::-webkit-scrollbar { display: none; }

/* Wider lobby pill columns to keep tap accuracy on long plans. The
   deck's .matrix-col floor is 6px (it scrolls). The lobby keeps a
   24px floor so every pill is finger-tap-able even before the
   horizontal scroll fallback kicks in. */
.lobby-matrix-inner .matrix-col {
  min-width: 24px;
  max-width: 44px;
}

/* Wave 5 lobby fixes — pills sized closer to the Apple HIG 44px hit
   target. The deck matrix uses 20-22px pills because they coexist with
   a tight active-slide header; the lobby matrix sits at the top of the
   screen with breathing room and is a primary jump-to control, so
   bigger is better. We bump the lobby pill height to 36px and retain
   touch-action: manipulation to suppress the iOS double-tap-zoom
   delay. Combined with horizontal pseudo-element padding this gives a
   ~36×(width + 4) hit zone with no cross-row bleed (the safe direction
   to expand is intentionally vertical-INSIDE so circuit pill rounds
   don't intercept each other's taps). */
.lobby-matrix-inner .pill {
  min-height: 36px;
  height: 36px;
  cursor: pointer;
  -ms-touch-action: manipulation;
  touch-action: manipulation;
  /* Stop iOS Safari from delaying the click 300ms — keeps neighbour
     activation rare and snaps tap → click. */
}

.lobby-matrix-inner .matrix-circuit {
  display: grid;
  grid-template-columns: repeat(var(--circuit-cols, 1), 1fr);
  gap: 3px;
  flex: var(--circuit-weight, 1) 1 0;
  /* Hotfix round 2 — Fix 1 — strip the coral backdrop on circuit
     blocks in the lobby pill matrix. The deck still uses
     `var(--c-brand-tint-bg)` here (deck CSS scoped under `#progress-matrix`)
     but the lobby's pre-workout matrix should sit on the dark surface
     with no coloured backdrop; the pills' own coral is the only accent. */
  background: transparent;
  border-radius: var(--radius-md);
  /* Round 6 — Fix 4: zero vertical padding so circuit pills share the
     SAME top baseline as standalone pills. Pre-Round-6 rule was
     `padding: 3px` which pushed circuit pills 3px down vs the
     standalone-pill row, breaking the matrix's row baseline that Carl
     reported on his device. Horizontal padding kept at 3px so the
     circuit's grid still has breathing room at its left/right edges
     (and the min/max width formulas below still hold).
     `align-self: start` hugs the circuit block to the top of its flex
     line so a multi-row circuit doesn't get stretched to its sibling's
     max height; `align-items: start` on the grid keeps each pill at
     the top of its track. Combined: first-row circuit pills now share
     the matrix-inner baseline with standalone pills. */
  padding: 0 3px;
  align-self: start;
  align-items: start;
  /* Mirror the matrix-col floor — 24px per circuit column — so each
     circuit pill is finger-tap-able even on long plans. */
  min-width: calc(24px * var(--circuit-cols, 1) + 3px * (var(--circuit-cols, 1) - 1) + 6px);
  max-width: calc(44px * var(--circuit-cols, 1) + 3px * (var(--circuit-cols, 1) - 1) + 6px);
}

.lobby-matrix-inner .matrix-circuit-row {
  display: contents;
}

/* Hotfix round 3 — Fix A — REAL strip of the coral backdrop on the
   lobby pill matrix.
   ROOT CAUSE of the still-visible coral wash on Carl's iPhone screenshot
   after PR 235: the deck's `.matrix-circuit-row::before` rule (line ~429)
   paints a coral-tint band behind every circuit row via an absolutely-
   positioned pseudo-element. PR 235 only zeroed the parent `.matrix-circuit`
   background — it never touched the per-row `::before` painter.
   Even with `display: contents` on the row, Safari still renders the
   row's `::before` pseudo (the spec leaves contents-element pseudos
   underspecified; iOS Safari paints them). Explicitly disable the
   pseudo in the lobby context so the matrix sits on the dark surface
   with only the pills themselves carrying coral. */
.lobby-matrix-inner .matrix-circuit-row::before,
.lobby-matrix-inner .matrix-circuit-row::after {
  content: none;
  background: transparent;
  display: none;
}

/* Hero list --------------------------------------------------------------- */
.lobby-list {
  list-style: none;
  margin: 16px 0 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: 10px;
}

.lobby-row {
  /* Closed-loop wave (2026-05-05): legacy gutter layout dropped.
     All rows align with 16px effective horizontal indent so heroes
     sit on one vertical line. In-circuit rows override with 14px
     padding + 2px frame border = same 16px effective position. */
  display: flex;
  align-items: stretch;
  gap: 0;
  background: transparent;
  border: 0;
  border-radius: 0;
  padding: 0 16px;
  transition: none;
}

/* Round 6 — Fix 3: standalone rows now share the in-circuit gutter
   width so all cards sit on one vertical line. The gutter cell on a
   standalone row stays empty (no rail, no connector — those are added
   by the `.in-circuit` selector below) but reserves the same 22px so
   the content column starts at the same X as circuit-row cards.
   Carl's directive: "every row gets the same indent; standalone +
   circuit rows shouldn't sit on different vertical columns".
   Pre-Round-6: this rule used `display: none` to collapse the gutter
   for standalones, which left them flush with the page padding while
   circuit rows sat 56px in (Round 5 widened the circuit gutter to ~3×
   the original 22px). All rows now reserve 56px so cards sit on one
   vertical line regardless of in-circuit state. */
/* Legacy gutter (rail + connector chrome for the old tree-branch
   design) is now display:none on EVERY row. The closed-loop frame
   replaces the rail metaphor; standalone rows no longer reserve the
   56px column either, so all exercise heroes sit at the same X
   position regardless of in-circuit state. */
.lobby-row > .lobby-row-gutter {
  display: none;
}

/* Round 6 — Fix 3 — keep rail / connector children hidden by default.
   They render inline empty <span>s in standalone rows otherwise; the
   `.in-circuit` selectors below promote them to absolute-positioned
   coral pieces only when the row is part of a circuit. */
.lobby-row > .lobby-row-gutter > .lobby-row-gutter-rail,
.lobby-row > .lobby-row-gutter > .lobby-row-gutter-connector {
  display: none;
}
.lobby-row > .lobby-row-content {
  flex: 1 1 auto;
  display: flex;
  align-items: stretch;
  gap: 12px;
  background: var(--c-surface-base);
  border: 1px solid var(--c-surface-border);
  border-radius: var(--radius-lg);
  padding: 10px;
  min-width: 0;
  transition: border-color var(--dur-fast) var(--ease-standard);
}

/* Hotfix round 2 — Fix 2(a) — strip the coral backdrop on circuit
   rows. The visual grouping is conveyed by the coral tree-branch rail
   (the gutter column on each in-circuit row), not a tinted row
   background. The legacy `.lobby-row.is-circuit` selector stays
   no-op'd here for clarity; the rail-rendering hooks live on the
   alias `.in-circuit` so both classes can be present without any
   coral fill leaking back.
   Hotfix round 3 — Fix B — chrome moved from `.lobby-row` to
   `.lobby-row-content` (the inner card). All visual states
   (active-pill border, rest tint) target the content column. */
.lobby-row.is-circuit > .lobby-row-content {
  background: var(--c-surface-base);
  border-color: var(--c-surface-border);
}

.lobby-row.is-rest > .lobby-row-content {
  background: var(--c-rest-tint-bg);
  border-color: var(--c-rest-tint-border);
  align-items: center;
  padding: 14px 16px;
}

.lobby-row.is-active-pill > .lobby-row-content {
  /* Visual coupling — lifts when the matched pill becomes the matrix
     "active" pill via the scroll reducer. */
  border-color: var(--c-brand);
  box-shadow: 0 0 0 1px var(--c-brand);
}

/* Hero ------------------------------------------------------------------- */
.lobby-hero {
  flex: 0 0 40%;
  aspect-ratio: 1 / 1;
  position: relative;
  overflow: hidden;
  border-radius: var(--radius-md);
  background: var(--c-surface-raised);
}

.lobby-hero-media,
.lobby-hero-skeleton {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
}

.lobby-hero-media {
  object-fit: cover;
  display: block;
}

.lobby-hero-media.is-grayscale {
  filter: grayscale(1) contrast(1.05);
}

.lobby-hero-skeleton {
  background: linear-gradient(
    90deg,
    var(--c-surface-raised) 0%,
    var(--c-surface-border) 50%,
    var(--c-surface-raised) 100%
  );
  background-size: 200% 100%;
  animation: lobby-shimmer 1.4s ease-in-out infinite;
}

@keyframes lobby-shimmer {
  0%   { background-position: 200% 0; }
  100% { background-position: -200% 0; }
}

@media (prefers-reduced-motion: reduce) {
  .lobby-hero-skeleton { animation: none; }
}

/* Info column ----------------------------------------------------------- */
.lobby-info {
  flex: 1 1 auto;
  display: flex;
  flex-direction: column;
  justify-content: center;
  gap: 4px;
  min-width: 0;
}

/* Hotfix round 2 — Fix 3 — row index numeral chrome removed.
   The `.lobby-info-titlebar` flex container + `.lobby-info-num` numeral
   are gone. Default-named exercises ("Exercise 1", "Exercise 2") already
   convey position via Studio's auto-naming; renamed exercises trust the
   practitioner. Net effect: `.lobby-info-name` is once again the sole
   first child of `.lobby-info`. */

.lobby-info-name {
  font-family: var(--f-display);
  font-size: 18px;
  font-weight: 700;
  line-height: 1.2;
  margin: 0;
  color: var(--c-ink-primary);
  letter-spacing: -0.01em;
  word-break: break-word;
}

.lobby-info-dose {
  font-family: var(--f-body);
  font-size: 13px;
  font-weight: 400;
  color: var(--c-ink-secondary);
  margin: 0;
  line-height: 1.4;
}

.lobby-info-notes {
  margin: 4px 0 0;
  font-family: var(--f-body);
  font-size: 12px;
  font-style: italic;
  color: var(--c-ink-muted);
  line-height: 1.45;
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
  cursor: pointer;
  background: none;
  border: 0;
  padding: 0;
  text-align: left;
  width: 100%;
}

.lobby-info-notes.is-expanded {
  -webkit-line-clamp: unset;
  display: block;
  overflow: visible;
}

/* Circuit group --------------------------------------------------------- */
/* Closed-loop circuit container (Wave: Lobby Circuit Loop, 2026-05-05).
   Replaced the prior tree-branch rail (left vertical + horizontal spurs
   to each exercise + L-corner on the last row) with a single rounded-
   rectangle FRAME around the whole circuit group. The frame is the
   visual loop; the cycles chip (`× N`) in the header carries iteration.
   No more spurs to inner exercises — they float inside the loop's tint.

   Layout:

     ┌─────────────────────────────────────┐  ← rounded top corners
     │ THE GAUNTLET                  × 3   │  ← header inside the frame
     │  [Push-ups card]                    │
     │  [Squats card]                      │
     │  [Planks card]                      │
     └─────────────────────────────────────┘  ← rounded bottom corners

   Why drop the rail+spurs:
   - "× N" alone was ambiguous about WHICH exercises repeat. The frame
     visually scopes the iteration to its contained rows.
   - Per-row spurs read as "decoration" not "structure". The loop reads
     as structure — the exercises are inside a container.
   - Carl: "I want the visual to more reflect an actual circuit to make
     people understand easier that there is repetition here." The
     closed loop is a literal circuit. */
/* Lanes wave (v54) — circuit rendered as a single <li class="lobby-circuit">
   hosting an SVG lanes overlay + a .lobby-circuit-frame containing the
   header (surface-2, isolated) + the body (coral-tinted, where the
   in-circuit rows live as <div>s). N coral rounded-rectangle outlines
   are drawn into the SVG by lobby.js's renderCircuitLanes() — one per
   round — plus an animated tracer that spirals from the innermost lane
   outward. Geometry mocked up at docs/design/mockups/lobby-circuit-lanes.html.

   Why one container instead of fragmented borders across sibling LIs:
   PRs #257/#258 split the frame across <li>s but the browser auto-
   closed the outer <li> when it encountered the nested <li> rows,
   ejecting them from the container in the parsed DOM. v54 fixes both
   issues — single LI with the rows as <div>s inside its body. */
.lobby-circuit {
  position: relative;
  list-style: none;
  margin: 8px 0;
  /* padding is set per-instance by JS to lanePad (= 5 * cycles), so the
     SVG (inset:0) extends outward from the frame by lanePad on each side. */
  padding: 0;
}

.lobby-circuit-lanes {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  pointer-events: none;
  overflow: visible;
  z-index: 0;
}

.lobby-circuit-lanes .lane-static {
  fill: none;
  stroke: rgba(255, 107, 53, 0.60);
  stroke-width: 2;
  stroke-linejoin: round;
}

.lobby-circuit-lanes .lane-tracer {
  fill: none;
  stroke: rgba(255, 107, 53, 0.95);
  stroke-width: 2.5;
  stroke-linecap: round;
  stroke-linejoin: round;
  filter: drop-shadow(0 0 4px rgba(255, 107, 53, 0.5));
  animation: lobby-circuit-tracer var(--dur, 27s) linear infinite;
}

@keyframes lobby-circuit-tracer {
  /* 0–90% of the timeline: tracer draws from start to end of the spiral. */
  0%   { stroke-dashoffset: var(--path-len); }
  90%  { stroke-dashoffset: 0; }
  /* 90–100%: hold at the end → brief settle before looping back. */
  100% { stroke-dashoffset: 0; }
}

@media (prefers-reduced-motion: reduce) {
  .lobby-circuit-lanes .lane-tracer {
    animation: none;
    stroke-dashoffset: 0;
  }
}

.lobby-circuit-frame {
  position: relative;
  z-index: 1;
  border-radius: 18px;
  overflow: hidden;
}

.lobby-circuit-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 8px;
  padding: 10px 16px;
  background: var(--c-surface-base);
  border-bottom: 1px solid rgba(255, 107, 53, 0.30);
}

.lobby-circuit-header-label {
  flex: 1 1 auto;
  font-family: var(--f-body);
  font-size: 14px;
  font-weight: 600;
  letter-spacing: 0.5px;
  color: var(--c-brand);
  text-transform: uppercase;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.lobby-circuit-header-cycles {
  flex: 0 0 auto;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  height: 22px;
  padding: 0 8px;
  background: var(--c-brand-tint-bg);
  border: 1px solid var(--c-brand-tint-border);
  border-radius: 9999px;
  font-family: "JetBrains Mono", ui-monospace, Menlo, Courier, monospace;
  font-size: 12px;
  font-weight: 600;
  color: var(--c-brand);
}

.lobby-circuit-body {
  background: rgba(255, 107, 53, 0.04);
  padding: 8px 16px 14px;
  display: flex;
  flex-direction: column;
  gap: 6px;
}

/* In-circuit rows now render as <div>s inside the body. Padding handled
   by the body itself; rows have no horizontal padding of their own to
   avoid double-indenting. */
.lobby-circuit-body .lobby-row {
  padding: 0;
}

/* Rest row -------------------------------------------------------------- */
.lobby-row.is-rest .lobby-rest-label {
  font-family: var(--f-body);
  font-size: 14px;
  font-weight: 600;
  color: var(--c-rest);
  letter-spacing: 0.01em;
}

/* Sticky CTA bar ------------------------------------------------------- */
/*
 * Round 4 (2026-05-04) — treatment selector moved behind the gear.
 * Bar is now Start + gear in a single row. The treatment row lives
 * inside `#lobby-settings-popover` which is `position: absolute`
 * inside `.lobby-cta-bar` and opens UPWARD from above the gear.
 */
.lobby-cta-bar {
  position: fixed;
  bottom: 0;
  left: 0;
  right: 0;
  z-index: 60;
  background: rgba(15, 17, 23, 0.92);
  -webkit-backdrop-filter: blur(14px);
  backdrop-filter: blur(14px);
  border-top: 1px solid var(--c-surface-border);
  padding: 12px 16px calc(12px + var(--safe-bottom));
}

.lobby-cta-inner {
  max-width: 720px;
  margin: 0 auto;
  display: flex;
  flex-direction: row;
  align-items: center;
  gap: 8px;
}

.lobby-treatment-row {
  display: flex;
  gap: 6px;
  height: 36px;
  background: var(--c-surface-base);
  border-radius: var(--radius-sm);
  padding: 3px;
  border: 1px solid var(--c-surface-border);
}

.lobby-treatment-row > button {
  flex: 1 1 0;
  background: transparent;
  border: 0;
  color: var(--c-ink-secondary);
  font-family: var(--f-body);
  font-size: 13px;
  font-weight: 600;
  border-radius: 6px;
  cursor: pointer;
  transition: background var(--dur-fast) var(--ease-standard),
              color var(--dur-fast) var(--ease-standard);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 6px;
  position: relative;
}

.lobby-treatment-row > button[aria-checked="true"] {
  background: var(--c-brand);
  color: #0F1117;
}

.lobby-treatment-row > button[data-locked="true"] {
  color: var(--c-ink-muted);
}

.lobby-treatment-row > button[data-locked="true"] .lobby-lock {
  display: inline-flex;
  width: 12px;
  height: 12px;
  opacity: 0.7;
}

.lobby-treatment-row > button:not([data-locked="true"]) .lobby-lock {
  display: none;
}

.lobby-start-btn {
  flex: 1 1 auto;
  height: 50px;
  background: var(--c-brand);
  color: #0F1117;
  border: 0;
  border-radius: var(--radius-md);
  font-family: var(--f-body);
  font-size: 16px;
  font-weight: 700;
  letter-spacing: 0.01em;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 8px;
  cursor: pointer;
  transition: background var(--dur-fast) var(--ease-standard);
}

.lobby-start-btn:hover { background: var(--c-brand-dark); }

.lobby-start-btn svg {
  width: 18px;
  height: 18px;
}

/* Lobby gear button — sits to the right of Start in the sticky CTA
   bar. 48×48 hit target, transparent surface, dim icon at rest, coral
   when the popover is open. Mirrors the deck's #btn-settings shape but
   ships its own coords + colour because the chrome contexts differ
   (deck = absolute on top of video; lobby = inline in the bar). */
.lobby-gear-btn {
  flex: 0 0 48px;
  width: 48px;
  height: 48px;
  border: 1px solid var(--c-surface-border);
  border-radius: var(--radius-md);
  background: var(--c-surface-base);
  color: var(--c-ink-secondary);
  padding: 0;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
  transition: background var(--dur-fast) var(--ease-standard),
              color var(--dur-fast) var(--ease-standard),
              border-color var(--dur-fast) var(--ease-standard);
}
.lobby-gear-btn:hover {
  background: var(--c-surface-raised);
  color: var(--c-ink-primary);
}
.lobby-gear-btn:active { transform: scale(0.96); }
.lobby-gear-btn:focus-visible {
  outline: 2px solid var(--c-brand);
  outline-offset: 2px;
}
.lobby-gear-btn[aria-expanded="true"] {
  color: var(--c-brand);
  border-color: var(--c-brand);
}
.lobby-gear-btn svg {
  width: 22px;
  height: 22px;
  display: block;
  stroke: currentColor;
}

/* Lobby share button — sits next to the gear, triggers the lobby PNG
   export + iOS share sheet (Wave Free Lobby Export, 2026-05-05). Mirrors
   .lobby-gear-btn shape so the two buttons read as one cluster. */
.lobby-share-btn {
  flex: 0 0 48px;
  width: 48px;
  height: 48px;
  border: 1px solid var(--c-surface-border);
  border-radius: var(--radius-md);
  background: var(--c-surface-base);
  color: var(--c-ink-secondary);
  padding: 0;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
  transition: background var(--dur-fast) var(--ease-standard),
              color var(--dur-fast) var(--ease-standard),
              border-color var(--dur-fast) var(--ease-standard);
}
.lobby-share-btn:hover {
  background: var(--c-surface-raised);
  color: var(--c-ink-primary);
}
.lobby-share-btn:active { transform: scale(0.96); }
.lobby-share-btn:focus-visible {
  outline: 2px solid var(--c-brand);
  outline-offset: 2px;
}
.lobby-share-btn:disabled {
  opacity: 0.5;
  cursor: progress;
}
.lobby-share-btn svg {
  width: 20px;
  height: 20px;
  display: block;
  stroke: currentColor;
}

/* Hide page chrome that shouldn't appear in the exported PNG. The
   .is-exporting class is toggled on <html> during snapshot capture. */
html.is-exporting .lobby-cta-bar,
html.is-exporting .lobby-meta-version,
html.is-exporting #lobby-settings-popover {
  display: none !important;
}
html.is-exporting .lobby-circuit-lanes .lane-tracer {
  /* Snapshot a quiet "settled" frame — pause the spiral animation
     mid-render via animation: none. */
  animation: none !important;
  stroke-dashoffset: 0 !important;
}
html.is-exporting .lobby-export-footer {
  display: flex !important;
}
.lobby-export-footer {
  display: none;
  padding: 16px;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  border-top: 1px solid var(--c-surface-border);
  margin-top: 12px;
}
.lobby-export-footer .lobby-export-brand {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  color: var(--c-ink-primary);
  font-family: 'Montserrat', -apple-system, sans-serif;
  font-weight: 700;
  font-size: 13px;
  letter-spacing: 0.3px;
}
.lobby-export-footer .lobby-export-brand-mark {
  width: 22px;
  height: 22px;
  background: var(--c-brand);
  border-radius: 4px;
  display: inline-block;
}
.lobby-export-footer .lobby-export-tagline {
  color: var(--c-ink-secondary);
  font-family: 'Inter', -apple-system, sans-serif;
  font-size: 11px;
  text-align: right;
}

/* Desktop export modal (Wave Free Lobby Export hardening, 2026-05-05).
   Shown after the snapshot completes when navigator.share isn't available
   (macOS Safari/Chrome desktop). User clicks Download anchor → fresh
   user-activation → browser honours the download. */
.lobby-export-modal[hidden] { display: none; }
.lobby-export-modal {
  position: fixed;
  inset: 0;
  z-index: 1000;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 24px;
}
.lobby-export-modal-backdrop {
  position: absolute;
  inset: 0;
  background: rgba(15, 17, 23, 0.85);
  backdrop-filter: blur(8px);
  -webkit-backdrop-filter: blur(8px);
}
.lobby-export-modal-card {
  position: relative;
  background: var(--c-surface);
  border: 1px solid var(--c-border);
  border-radius: 16px;
  padding: 20px;
  max-width: min(520px, 100%);
  max-height: calc(100vh - 48px);
  display: flex;
  flex-direction: column;
  gap: 16px;
  box-shadow: 0 16px 48px rgba(0, 0, 0, 0.45);
}
.lobby-export-modal-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
}
.lobby-export-modal-title {
  margin: 0;
  font-family: 'Montserrat', -apple-system, sans-serif;
  font-size: 16px;
  font-weight: 600;
  color: var(--c-ink-primary);
}
.lobby-export-modal-close {
  background: transparent;
  border: 0;
  color: var(--c-ink-secondary);
  font-size: 24px;
  line-height: 1;
  cursor: pointer;
  padding: 4px 8px;
  border-radius: 6px;
}
.lobby-export-modal-close:hover {
  color: var(--c-ink-primary);
  background: rgba(255, 255, 255, 0.05);
}
.lobby-export-modal-img {
  width: 100%;
  height: auto;
  max-height: calc(100vh - 220px);
  object-fit: contain;
  border-radius: 8px;
  background: #0F1117;
}
.lobby-export-modal-actions {
  display: flex;
  flex-direction: column;
  gap: 8px;
  align-items: stretch;
}
.lobby-export-modal-download {
  display: inline-block;
  text-align: center;
  background: var(--c-accent);
  color: #0F1117;
  font-family: 'Montserrat', -apple-system, sans-serif;
  font-weight: 600;
  font-size: 14px;
  padding: 12px 16px;
  border-radius: 999px;
  text-decoration: none;
  cursor: pointer;
}
.lobby-export-modal-download:hover {
  filter: brightness(1.05);
}
.lobby-export-modal-hint {
  margin: 0;
  text-align: center;
  color: var(--c-ink-secondary);
  font-family: 'Inter', -apple-system, sans-serif;
  font-size: 12px;
}

/* Lobby settings popover — anchored to the viewport-bottom and opens
   UPWARD from the gear. `position: absolute` inside `.lobby-cta-bar`
   so it reads "stuck to the bar" rather than free-floating. The bar
   has `z-index: 60`; this lives inside it, no conflict.

   Anchored to the right edge of the bar's inner container (where the
   gear sits). Width grows to a comfortable popover size on phone +
   tablet; max-width clamps it on desktop. */
.lobby-settings-popover {
  position: absolute;
  /* Float above the bar — bar's top padding is 12px; we sit just above
     it with an 8px gap. `bottom: 100%` aligns the popover's bottom
     edge to the bar's top edge; `+8px` lift comes from translateY. */
  bottom: calc(100% + 8px);
  right: 16px;
  z-index: 70;
  min-width: 240px;
  max-width: calc(100vw - 32px);
  background: rgba(26, 29, 39, 0.96);
  border: 1px solid var(--c-brand);
  border-radius: var(--radius-md);
  padding: 12px 14px;
  box-shadow: 0 10px 30px rgba(0, 0, 0, 0.45);
  -webkit-backdrop-filter: blur(12px);
  backdrop-filter: blur(12px);
  font-family: var(--f-body);
  color: var(--c-ink-primary);
  /* Animation on open — match the deck's settings-popover convention. */
  opacity: 0;
  transform: translateY(6px);
  pointer-events: none;
  transition: opacity 160ms var(--ease-standard),
              transform 160ms var(--ease-standard);
}
.lobby-settings-popover[data-open="true"] {
  opacity: 1;
  transform: translateY(0);
  pointer-events: auto;
}
/* HTML `hidden` attribute — keep the element off the a11y tree until
   it opens (popover starts hidden). When JS sets data-open + clears
   `hidden`, the transition above runs. */
.lobby-settings-popover[hidden] { display: none; }

.lobby-settings-popover-title {
  font-family: var(--f-display);
  font-weight: 700;
  font-size: 11px;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--c-ink-secondary);
  margin-bottom: 8px;
}

/* When the popover IS the host of the treatment row, give the row a
   little more breathing room so each pill is comfortably tappable
   (≥44 px hit target on phones). The bar context squeezed it to 36px;
   here we can afford 40. */
.lobby-settings-popover .lobby-treatment-row {
  height: 40px;
}

/* Self-grant modal ----------------------------------------------------- */
.lobby-self-grant-modal {
  position: fixed;
  inset: 0;
  z-index: 200;
  display: flex;
  align-items: flex-end;
  justify-content: center;
}

.lobby-self-grant-backdrop {
  position: absolute;
  inset: 0;
  background: rgba(0, 0, 0, 0.55);
  -webkit-backdrop-filter: blur(2px);
  backdrop-filter: blur(2px);
}

.lobby-self-grant-card {
  position: relative;
  width: 100%;
  max-width: 480px;
  background: var(--c-surface-base);
  border: 1px solid var(--c-surface-border);
  border-top-left-radius: var(--radius-xl);
  border-top-right-radius: var(--radius-xl);
  padding: 24px 20px calc(24px + var(--safe-bottom));
  margin: 0 auto;
}

.lobby-self-grant-title {
  font-family: var(--f-display);
  font-size: 20px;
  font-weight: 700;
  margin: 0 0 12px;
  color: var(--c-ink-primary);
  letter-spacing: -0.01em;
}

.lobby-self-grant-identity {
  font-family: var(--f-body);
  font-size: 14px;
  color: var(--c-ink-secondary);
  margin: 0 0 12px;
}

.lobby-self-grant-identity strong {
  color: var(--c-ink-primary);
  font-weight: 700;
}

.lobby-self-grant-body {
  font-family: var(--f-body);
  font-size: 14px;
  color: var(--c-ink-secondary);
  line-height: 1.5;
  margin: 0 0 18px;
}

.lobby-self-grant-error {
  font-family: var(--f-body);
  font-size: 13px;
  color: var(--c-error);
  margin: 0 0 12px;
}

.lobby-self-grant-actions {
  display: flex;
  gap: 10px;
}

.lobby-self-grant-btn {
  flex: 1 1 0;
  height: 44px;
  border-radius: var(--radius-md);
  border: 1px solid var(--c-surface-border);
  background: transparent;
  color: var(--c-ink-primary);
  font-family: var(--f-body);
  font-size: 15px;
  font-weight: 600;
  cursor: pointer;
  transition: background var(--dur-fast) var(--ease-standard);
}

.lobby-self-grant-cancel:hover {
  background: var(--c-surface-raised);
}

.lobby-self-grant-allow {
  background: var(--c-brand);
  border-color: var(--c-brand);
  color: #0F1117;
}

.lobby-self-grant-allow:hover {
  background: var(--c-brand-dark);
  border-color: var(--c-brand-dark);
}

.lobby-self-grant-allow:disabled,
.lobby-self-grant-allow[aria-disabled="true"] {
  opacity: 0.6;
  cursor: progress;
}

/* Wider screens — same max-width 720, centred. */
@media (min-width: 600px) {
  .lobby-meta { padding: 28px 4px 16px; }
}

/* Reduced motion — disable shimmer + transitions on hover/border. */
@media (prefers-reduced-motion: reduce) {
  .lobby-row > .lobby-row-content,
  .lobby-treatment-row > button,
  .lobby-start-btn,
  .lobby-self-grant-btn { transition: none; }
}
