/* =============================================================
 * Frisson Mobile — M12D / MO-1 foundation layer
 *
 * Loaded AFTER style.css (the monolithic desktop Noir sheet).
 * This file is the mobile substrate every later mobile stream
 * (MO-2 chrome, MO-3 detail content, forms, auth) hangs off.
 *
 * Adaptation note: the handoff shipped this as two drop-in files
 * (`tokens-mobile.css` + `components-mobile.css`) meant to live
 * inside a multi-file CSS setup. This app has ONE monolithic
 * style.css, so both are consolidated here as the new mobile.css,
 * linked after style.css. (MO-1 structural decision.)
 *
 * Rules honoured from the project guardrails:
 *   • NO hardcoded #hex — every colour resolves through a token.
 *     Handoff hex (#181f3e etc.) mapped to the nearest canonical
 *     Noir token (--cream-mist / --glass-* / accent -rgb triplets).
 *   • Accent translucency via rgba(var(--*-rgb), a) using the
 *     existing -rgb triplets in style.css :root.
 *   • Neutral overlays rgba(238,240,250,a) / rgba(0,0,0,a) kept as
 *     literals — that is the established style.css convention (50+
 *     existing uses); no --text-light-rgb token exists and adding
 *     one would touch the shared :root (out of MO-1 scope).
 *   • Layout layer guarded behind @media (max-width: 480px) so the
 *     desktop (≥720px) experience is byte-for-byte unchanged.
 *   • Tap targets ≥44px under @media (pointer: coarse) — P-030.
 *   • prefers-reduced-motion → instant snaps via --m13-sheet-transition.
 *
 * Section index:
 *   T.  :root token additions (--m13-*)
 *   1.  .fm-mobile               — root container
 *   2.  .fm-mobile-header        — top pill bar + search/menu buttons
 *   3.  .fm-mobile-header--search — search-input takeover state
 *   4.  .fm-mobile-test-banner   — TEST MODE pill
 *   5.  .fm-side-btn             — left/right side buttons on map
 *   6.  .fm-fab                  — primary FAB above tab bar
 *   7.  .fm-tab-bar              — bottom navigation
 *   8.  .fm-tab-panel            — fullscreen tab list overlay
 *   9.  .fm-sheet                — bottom sheet (peek / half / full)
 *   10. .fm-sheet::before        — tone-coded glow on top edge
 *   11. .fm-sheet__handle        — drag handle area
 *   12. .fm-peek-header          — common header inside sheet body
 *   13. .fm-attendance           — avatar stack + RSVP counts
 *   14. .fm-rsvp                 — Going / Interested sibling pair
 *   15. (removed ADR-255)        — dead .fm-discussion-link rows
 *   16. .fm-form-fullscreen      — fullscreen form chrome
 *   17. .fm-quick-menu           — long-press / FAB action menu
 *   18. .fm-fan-popup            — small inline popup for single fan moment
 *   Z.  @media (pointer: coarse) — 44px touch floor
 * ============================================================= */

/* =============================================================
 * T. TOKEN ADDITIONS — --m13-* prefix (mobile chrome/sheet only)
 *
 * Harmless additive variables; left unguarded so JS can read the
 * snap ratios at any width. The desktop layout never references
 * any --m13-* token, so this cannot affect ≥720px.
 * ============================================================= */
:root {
  /* ── Z-INDEX SCALE — mobile layering ──────────────────────
   * chrome → backdrop → sheet → tab bar → tab list panel.
   * Tab bar is intentionally ABOVE the sheet (always visible).
   * Tab list panel is the highest — it overlays even the header. */
  /* Base +1000: this app's Leaflet map container sits at z-index 1000 and
   * its floating controls at 1010–1100, so the handoff's 25–80 scale was
   * BELOW the map → the map repainted over all mobile chrome (chrome
   * "flashed then vanished" once Leaflet finished initialising). Lift the
   * whole scale above the map while preserving the internal ordering the
   * cross-stream review verified. Desktop never references these tokens. */
  --m13-z-chrome:     1025;  /* header, test banner, side buttons, FAB */
  --m13-z-backdrop:   1030;  /* dim layer fading in as sheet expands */
  --m13-z-sheet:      1040;  /* bottom sheet itself */
  --m13-z-tab-bar:    1045;  /* bottom nav — always on top of sheet */
  --m13-z-tab-panel:  1070;  /* fullscreen tab list overlay */
  --m13-z-quick-menu: 1080;  /* one-shot quick menus (long-press, FAB) */

  /* ── SHEET SNAP HEIGHTS ────────────────────────────────────
   * Three discrete positions, computed at runtime from viewport
   * minus tab bar; these ratios are the source of truth. */
  --m13-sheet-peek:        96px;   /* handle + header + action bar */
  --m13-sheet-half-ratio:  0.42;   /* of (viewport - tabBar) — medium "about a third"
                                      default open height (Petra 2026-05-26: sheets
                                      were too tall; full only on explicit drag-up) */
  --m13-sheet-tall-ratio:  0.62;   /* 4th "tall" snap (~3/5). Text-heavy detail
                                      (concert/online/POI) opens here so the copy
                                      isn't clipped (Petra 2026-05-27); leaves the
                                      top ~2/5 for the focused marker on the map. */
  --m13-sheet-full-offset: 8px;    /* gap from top of viewport at full */
  --m13-sheet-radius:      24px;   /* top corners of the sheet */
  /* Drag the handle DOWN past peek by this many px → dismiss the sheet
   * (mobile-native pull-to-close). Below this, it springs back to peek. */
  --m13-sheet-dismiss-px:  72px;
  /* TAB-LIST SHEET (the expanded tab = a draggable list sheet, Google-Maps
   * style). At "full" the sheet's top edge lands on the TOP edge of the TEST
   * MODE banner (safe-area + 64px) — so only the header stays above it and the
   * sheet rises OVER the TEST MODE banner (test mode is z-below the sheet =
   * "vespod", hidden behind it, never rendered on top). Petřin pokyn
   * 2026-05-27. A tab list still never goes the whole screen (header stays). */
  --m13-panel-full-top:    calc(env(safe-area-inset-top) + 64px);
  --m13-panel-half-ratio:  0.62;   /* "half" snap (drag-down stop) */

  /* ── MOBILE CHROME DIMENSIONS ──────────────────────────── */
  --m13-header-pill-h:      48px;
  --m13-header-pill-r:      22px;
  --m13-side-btn-size:      52px;  /* museum + note (left) */
  --m13-side-btn-help-size: 48px;  /* globe help (right) */
  --m13-fab-size:           60px;
  --m13-tab-bar-h:          64px;

  /* ── M14 MAP-ACTION CHROME (map-actions-handoff) ───────────
   * Shape-language fix: the three left side buttons (Landmark /
   * note / legend) become rounded-square tiles inside ONE glass
   * "action rail" capsule, and the round + FAB becomes a labeled
   * "+ MOMENT" capsule. Squares + a worded pill read as CONTROLS,
   * distinct from the round CLUSTER bubbles. Mockup:
   * docs/handoff/map-actions-handoff/mockups/01-chosen-combo.png. */
  /* M-rail-shrink (Petřin pokyn 2026-06-10 "zabírají moc prostoru na mobilu")
   * — tile 40→34, padding 8→6, gap 6→5 ⇒ capsule 56→46. Glyph + caption
   * shrink to match (svg 22→20 below; caption 10→9 in .map-actions__label). */
  --m14-rail-w:           46px;   /* capsule outer width (6 pad + 34 tile + 6 pad = 46) */
  --m14-rail-padding:     6px;    /* capsule inner padding (sym top+bottom) */
  --m14-rail-radius:      15px;   /* capsule outer radius (proportional to smaller tile) */
  --m14-rail-tile:        34px;   /* one rounded-square tile        */
  --m14-rail-tile-radius: 12px;   /* proportional to the smaller tile */
  --m14-rail-gap:         5px;    /* gap between tiles (dark capsule still peeks) */
  /* ADR-295 (Petřin pokyn 2026-06-10, iter 2 — "více nahoru, ale nikdy
   * nepřekrývat"): 78px collided with the satellite pills row (top+64px,
   * ~30px tall) and full vertical centering sat too low. 140px clears the
   * worst case = pills row + the TEST MODE pill that drops under it
   * (top+104px + ~28px). */
  --m14-rail-top:         140px;
  --m14-rail-left:        12px;
  /* (--m14-rail-tile-top / --m14-rail-tile-step removed by M13.X-PEBBLES,
   * ADR-247 — tiles are flex children of the #map-actions capsule now, no
   * per-tile fixed `top:` math.) */
  --m14-rail-caption-gap: 4px;    /* tile → micro-caption (ADR-247 R3; Petra
                                   * 2026-06-04 "mírné odsazení" — 4px keeps
                                   * the caption bound to ITS tile, still
                                   * clearly under the 6px slot gap) */

  --m14-fab-height:       52px;   /* just under original 60 — Petra 2026-05-30: 44 byl moc radikální, "na výšku zúžit jen trochu" */
  --m14-fab-radius:       26px;   /* half height → full pill        */
  --m14-fab-pad:          0 19px 0 15px;
  --m14-fab-gap:          4px;    /* tight "+ ECHO" (Petra 2026-05-28 — 8 was too far from icon) */
  --m14-fab-label-size:   13px;
  --m14-fab-label-track:  0.12em;
  --m14-fab-label-weight: 800;

  /* ── SHEET TONE-CODED EDGE GLOW ────────────────────────────
   * References existing accent tokens; one per detail type. */
  --m13-edge-teal:    var(--accent-turquoise);
  --m13-edge-iris:    var(--accent-violet);
  --m13-edge-gold:    var(--accent-copper);
  --m13-edge-plum:    var(--accent-violet);   /* gathering — same hue family */
  --m13-edge-magenta: var(--accent-mine);

  /* ── BACKDROP base colour (opacity set inline by JS) ────────
   * Deep ink wash. rgba(5,7,18) kept as literal — codebase uses
   * raw rgba for translucent ink layers; no exact token exists. */
  --m13-backdrop-color: rgba(5, 7, 18, 1);
}

/* Reduced motion — instant snaps, no spring (token consumed by
 * .fm-sheet transition). Default (no-preference) keeps the ease. */
@media (prefers-reduced-motion: reduce) {
  :root { --m13-sheet-transition: none; }
}
@media (prefers-reduced-motion: no-preference) {
  :root { --m13-sheet-transition: height 240ms cubic-bezier(0.22, 1, 0.36, 1); }
}

/* (M13.X-PEBBLES, ADR-247 — the decorative .fm-action-rail-frame is gone:
 * #map-actions itself is the mobile glass capsule now, a real flex column
 * whose height hugs the visible tiles. See section C inside the phone
 * media block below.) */

/* =============================================================
 * MOBILE LAYOUT LAYER — guarded ≤480px.
 *
 * Everything below only applies on phone-width viewports. The
 * .fm-* namespace is net-new (verified: style.css has zero .fm-*
 * selectors, only doc-comment references), so no collision with
 * the ~40 pre-existing mobile media queries in style.css.
 * ============================================================= */
/* M13.X-CHROME-REFLOW (Petřin pokyn 2026-06-03) — breakpoint lifted 480 → 767
   so the mobile chrome matches the desktop side, which already cuts over at
   `min-width: 768px` (style.css). The old 480px floor left a 481–767px hybrid
   band where the desktop hero + action rail still rendered AND the ≤767px
   hamburger appeared, so the floating chrome groups collided (Petřin narrow-
   window bug). Admin/backstage card styles below stay phone-only via a nested
   ≤480px guard — their kebab action-sheet is injected by adminMobile*.js at
   ≤480px and lifting the cards without that JS would leave rows actionless. */
@media (max-width: 767px) {

/* ── 1. .fm-mobile — root container ──────────────────────── */
.fm-mobile {
  position: relative;
  width: 100vw;
  max-width: 480px;
  margin: 0 auto;
  height: 100vh;
  height: 100dvh;
  background: var(--cream-base);
  overflow: hidden;
  font-family: var(--font-body);
  color: var(--text-dark);
}

/* ── 2. .fm-mobile-header — top bar (search · title · menu) ──
 * Three slots: 48px round button · flex-1 title pill · 48px round.
 * Title is gradient Cormorant + BETA chip. */
.fm-mobile-header {
  position: absolute;
  left: 0;
  right: 0;
  top: env(safe-area-inset-top);   /* iOS notch */
  padding: 6px 14px 0;
  z-index: var(--m13-z-chrome);
  display: flex;
  align-items: center;
  gap: 10px;
  pointer-events: none;            /* children re-enable */
}

.fm-mobile-header__btn {
  pointer-events: auto;
  width: var(--m13-header-pill-h);
  height: var(--m13-header-pill-h);
  flex-shrink: 0;
  border-radius: 50%;
  background: linear-gradient(180deg, var(--glass-strong), var(--glass-soft));
  backdrop-filter: blur(18px) saturate(180%);
  -webkit-backdrop-filter: blur(18px) saturate(180%);
  border: 1px solid var(--border-on-light);
  color: var(--text-dark);
  display: grid;
  place-items: center;
  cursor: pointer;
  box-shadow: 0 6px 20px rgba(0, 0, 0, 0.45),
              inset 0 1px 0 rgba(238, 240, 250, 0.06);
}

.fm-mobile-header__title {
  pointer-events: auto;
  flex: 1;
  min-width: 0;
  height: var(--m13-header-pill-h);
  border-radius: var(--m13-header-pill-r);
  padding: 0 12px;
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 8px;
  background: linear-gradient(180deg, var(--glass-strong), var(--glass-soft));
  backdrop-filter: blur(18px) saturate(180%);
  -webkit-backdrop-filter: blur(18px) saturate(180%);
  border: 1px solid var(--border-on-light);
  box-shadow: 0 8px 24px rgba(0, 0, 0, 0.45),
              inset 0 1px 0 rgba(238, 240, 250, 0.06);
}

.fm-mobile-header__wordmark {
  font-family: var(--font-display);
  font-style: italic;
  font-weight: 700;
  font-size: 22px;
  line-height: 1;
  letter-spacing: 0.005em;
  background-image: linear-gradient(95deg,
    var(--accent-violet) 0%,
    var(--text-dark) 38%,
    var(--accent-mine) 100%);
  -webkit-background-clip: text;
  background-clip: text;
  -webkit-text-fill-color: transparent;
  color: transparent;
  text-shadow: 0 0 18px rgba(var(--accent-violet-rgb), 0.20);
  white-space: nowrap;
}

/* ── 3. .fm-mobile-header--search — search input takeover ────
 * Title pill morphs into an input field; search button disappears,
 * × close button replaces the hamburger. Toggle a modifier on the
 * header to swap which children render. */
.fm-mobile-header__search-pill {
  pointer-events: auto;
  flex: 1;
  min-width: 0;
  height: var(--m13-header-pill-h);
  border-radius: var(--m13-header-pill-r);
  padding: 0 14px;
  display: flex;
  align-items: center;
  gap: 10px;
  background: linear-gradient(180deg, var(--glass-strong), var(--glass-soft));
  backdrop-filter: blur(18px) saturate(180%);
  -webkit-backdrop-filter: blur(18px) saturate(180%);
  border: 1px solid rgba(var(--accent-turquoise-rgb), 0.33);
  box-shadow: 0 8px 24px rgba(0, 0, 0, 0.45),
              0 0 0 3px rgba(var(--accent-turquoise-rgb), 0.13),
              inset 0 1px 0 rgba(238, 240, 250, 0.06);
}

.fm-mobile-header__search-pill input {
  flex: 1;
  min-width: 0;
  background: transparent;
  border: 0;
  outline: 0;
  color: var(--text-dark);
  font: 500 14px var(--font-body);
  caret-color: var(--accent-turquoise);
}

/* ── 4. .fm-mobile-test-banner — TEST MODE pill (solid gold) ──
 * Solid gold gradient with dark ink text. NEVER tint-on-glass —
 * earlier iteration was too transparent to read. */
.fm-mobile-test-banner {
  position: absolute;
  left: 50%;
  top: calc(env(safe-area-inset-top) + 64px);
  transform: translateX(-50%);
  z-index: var(--m13-z-chrome);
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 6px 14px 6px 8px;
  border-radius: 999px;
  background: linear-gradient(180deg, var(--accent-copper), var(--accent-copper-bright));
  border: 1px solid var(--accent-copper);
  color: var(--surface-ink);
  font: 700 11px/1 var(--font-body);
  letter-spacing: 0.22em;
  text-transform: uppercase;
  box-shadow: 0 6px 20px rgba(var(--accent-copper-rgb), 0.40),
              inset 0 1px 0 rgba(255, 255, 255, 0.18);
}

.fm-mobile-test-banner__check {
  width: 16px;
  height: 16px;
  border-radius: 4px;
  background: var(--surface-ink);
  color: var(--accent-copper);
  display: grid;
  place-items: center;
  font: 700 11px var(--font-body);
}

/* ── 5. .fm-side-btn — left/right floating map buttons ───────
 * Left stack: museum (POI menu, gold), note (song leaderboard, iris).
 * Right: globe (help, neutral). Tone matches action category. */
.fm-side-btn {
  pointer-events: auto;
  width: var(--m13-side-btn-size);
  height: var(--m13-side-btn-size);
  border-radius: 50%;
  border: 1px solid var(--border-on-light);
  display: grid;
  place-items: center;
  cursor: pointer;
  box-shadow: 0 6px 20px rgba(0, 0, 0, 0.45),
              inset 0 1px 0 rgba(238, 240, 250, 0.06);
}

.fm-side-btn--poi {
  background: radial-gradient(circle at 35% 30%,
    var(--accent-copper-deep) 0%, var(--surface-ink) 80%);
  color: var(--accent-copper);
  border-color: rgba(var(--accent-copper-rgb), 0.40);
}

.fm-side-btn--songs {
  background: radial-gradient(circle at 35% 30%,
    var(--accent-violet-deep) 0%, var(--surface-ink) 80%);
  color: var(--accent-violet-soft);
  border-color: rgba(var(--accent-violet-rgb), 0.40);
}

.fm-side-btn--help {
  width: var(--m13-side-btn-help-size);
  height: var(--m13-side-btn-help-size);
  background: radial-gradient(circle at 35% 30%,
    var(--cream-mist) 0%, var(--surface-ink) 80%);
  color: var(--text-muted);
}

.fm-side-stack {
  position: absolute;
  z-index: var(--m13-z-chrome);
  display: flex;
  flex-direction: column;
  gap: 12px;
}

.fm-side-stack--left  { left: 14px; }
.fm-side-stack--right { right: 14px; }

/* ── 6. .fm-fab — primary add FAB above tab bar ──────────────
 * Magenta gradient (= "your moment" tone). Tap → quick menu
 * (Add moment / Propose landmark). */
.fm-fab {
  position: absolute;
  right: 18px;
  bottom: calc(var(--m13-tab-bar-h) + 16px);
  z-index: var(--m13-z-chrome);
  width: var(--m13-fab-size);
  height: var(--m13-fab-size);
  border-radius: 50%;
  border: 1.5px solid var(--accent-mine);
  background: linear-gradient(180deg, var(--accent-mine), var(--accent-mine-deep));
  color: var(--text-light);
  display: grid;
  place-items: center;
  cursor: pointer;
  box-shadow: 0 0 22px rgba(var(--accent-mine-rgb), 0.40),
              0 10px 24px rgba(0, 0, 0, 0.55);
}

/* ── 7. .fm-tab-bar — bottom navigation (always visible) ─────
 * Four tabs: Moments / Concerts / Online / Yours. Active tab gets
 * a tone-coded glow strip + tinted background. NO "Map" tab. */
.fm-tab-bar {
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  height: var(--m13-tab-bar-h);
  padding: 4px 10px 6px;
  background: linear-gradient(180deg, var(--glass-strong), var(--glass-strong));
  backdrop-filter: blur(20px) saturate(180%);
  -webkit-backdrop-filter: blur(20px) saturate(180%);
  border-top: 1px solid var(--border-on-light);
  z-index: var(--m13-z-tab-bar);
  display: flex;
  align-items: stretch;
  gap: 4px;
}

.fm-tab {
  flex: 1;
  min-width: 0;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 3px;
  padding: 4px 2px;
  border-radius: 14px;
  border: 0;
  background: transparent;
  color: var(--text-muted);
  font: 500 10px var(--font-body);
  letter-spacing: 0.06em;
  text-transform: uppercase;
  cursor: pointer;
  position: relative;
  transition: background 200ms, color 200ms;
}

.fm-tab--active { font-weight: 700; }

.fm-tab--moments.fm-tab--active  { color: var(--accent-violet-soft); background: linear-gradient(180deg, rgba(var(--accent-violet-rgb), 0.13), transparent); }
.fm-tab--concerts.fm-tab--active { color: var(--accent-turquoise);     background: linear-gradient(180deg, rgba(var(--accent-turquoise-rgb), 0.13), transparent); }
.fm-tab--online.fm-tab--active   { color: var(--accent-online-bright); background: linear-gradient(180deg, rgba(var(--accent-online-rgb), 0.13), transparent); }
.fm-tab--yours.fm-tab--active    { color: var(--accent-mine);          background: linear-gradient(180deg, rgba(var(--accent-mine-rgb), 0.13), transparent); }

.fm-tab--active::before {
  content: "";
  position: absolute;
  top: 3px;
  left: 50%;
  transform: translateX(-50%);
  width: 24px;
  height: 3px;
  border-radius: 2px;
  background: currentColor;
  box-shadow: 0 0 8px currentColor;
}

/* ── 8. .fm-tab-panel — fullscreen tab list overlay ──────────
 * Tapping a tab slides this up; covers EVERYTHING (header, banner,
 * side buttons, FAB). Has its own header (title + count + ×). z=70
 * — above chrome. A detail sheet opens OVER the panel (rendered
 * inside the panel's stacking context). */
.fm-tab-panel {
  position: absolute;
  left: 0;
  right: 0;
  top: 0;
  bottom: var(--m13-tab-bar-h);
  z-index: var(--m13-z-tab-panel);
  background: linear-gradient(180deg, var(--cream-base) 0%, var(--cream-surface) 100%);
  border-top: 1px solid;
  display: flex;
  flex-direction: column;
  box-shadow: 0 -18px 50px rgba(0, 0, 0, 0.55);
}

.fm-tab-panel--moments  { border-top-color: rgba(var(--accent-violet-rgb), 0.33); }
.fm-tab-panel--concerts { border-top-color: rgba(var(--accent-turquoise-rgb), 0.33); }
.fm-tab-panel--online   { border-top-color: rgba(var(--accent-online-rgb), 0.33); }
.fm-tab-panel--yours    { border-top-color: rgba(var(--accent-mine-rgb), 0.33); }
/* M13.X-SONGS-TAB (ADR-237) — Songs = gold, the rating world's colour. */
.fm-tab-panel--songs    { border-top-color: rgba(var(--accent-copper-rgb), 0.33); }

.fm-tab-panel__head {
  /* Grip sits ABOVE the header now (the sheet starts mid-screen, not under
   * the notch), so no safe-area-top padding here. */
  padding: 2px 16px 12px;
  display: flex;
  align-items: center;
  gap: 10px;
  border-bottom: 1px solid var(--border-on-light);
}

.fm-tab-panel__rule {
  width: 4px;
  height: 22px;
  border-radius: 2px;
  flex-shrink: 0;
}

.fm-tab-panel--moments  .fm-tab-panel__rule { background: var(--accent-violet-soft); box-shadow: 0 0 8px var(--accent-violet-soft); }
.fm-tab-panel--concerts .fm-tab-panel__rule { background: var(--accent-turquoise);     box-shadow: 0 0 8px var(--accent-turquoise); }
.fm-tab-panel--online   .fm-tab-panel__rule { background: var(--accent-online-bright); box-shadow: 0 0 8px var(--accent-online-bright); }
.fm-tab-panel--yours    .fm-tab-panel__rule { background: var(--accent-mine);          box-shadow: 0 0 8px var(--accent-mine); }
.fm-tab-panel--songs    .fm-tab-panel__rule { background: var(--accent-copper-bright); box-shadow: 0 0 8px var(--accent-copper-bright); }

.fm-tab-panel__title {
  flex: 1;
  font-family: var(--font-display);
  font-style: italic;
  font-weight: 700;
  font-size: 22px;
  line-height: 1;
  color: var(--text-dark);
}

.fm-tab-panel__subtitle {
  font: 500 11px var(--font-body);
  color: var(--text-muted);
  margin-top: 3px;
  letter-spacing: 0.06em;
}

.fm-tab-panel__body {
  flex: 1;
  overflow-y: auto;
  padding: 6px 16px 14px;
  -webkit-overflow-scrolling: touch;
}

/* ── 9. .fm-sheet — bottom sheet (3 snap points + drag) ──────
 * Always positioned ABOVE the tab bar (bottom: tab-bar-h); grows
 * upward. Drag handle at the top inside; backdrop dims chrome
 * behind as the sheet grows. Height transition; reduced-motion
 * swaps to instant via --m13-sheet-transition. */
.fm-sheet {
  position: absolute;
  left: 0;
  right: 0;
  bottom: var(--m13-tab-bar-h);
  /* ONE continuous surface (Petřin pokyn 2026-05-30 — "jednolitá oblast, ne dvě
     oblasti / čára nad avatarem"). A SINGLE gradient with SOLID colour stops: the
     tone is baked INTO the top navy (color-mix of two solid colours, NOT a
     transparent overlay — transparent-alpha interpolation banded on iOS Safari),
     blending down to plain navy. overflow:hidden + border-radius clip it to the
     rounded top corners, so the tint reaches them with no strip / no break. */
  background: linear-gradient(180deg,
    color-mix(in srgb, var(--m13-edge-tone, var(--accent-turquoise)) 20%, var(--cream-surface)) 0%,
    var(--cream-surface) 140px);
  border-top-left-radius: var(--m13-sheet-radius);
  border-top-right-radius: var(--m13-sheet-radius);
  /* Lift off the dark map (black drop) + a faint inner top highlight. No tone
     halo above — the tone now lives INSIDE as the wash (see background). */
  box-shadow: 0 -18px 50px rgba(0, 0, 0, 0.42),
              inset 0 1px 0 rgba(238, 240, 250, 0.06);
  z-index: var(--m13-z-sheet);
  overflow: hidden;
  display: flex;
  flex-direction: column;
  transition: var(--m13-sheet-transition, height 240ms cubic-bezier(0.22, 1, 0.36, 1));
}

.fm-sheet--peek { height: var(--m13-sheet-peek); }
.fm-sheet--half { height: calc((100dvh - var(--m13-tab-bar-h)) * var(--m13-sheet-half-ratio)); }
.fm-sheet--tall { height: calc((100dvh - var(--m13-tab-bar-h)) * var(--m13-sheet-tall-ratio)); }
.fm-sheet--full { height: calc(100dvh - var(--m13-tab-bar-h) - var(--m13-sheet-full-offset)); }

/* ── 10. Sheet top tone — now an INTERNAL background wash (see .fm-sheet) ─────
 * The old .fm-sheet::before 1px glowing line was the "oddělený pruh" Petra
 * flagged (2026-05-30) — removed. The per-type --m13-edge-tone below feeds the
 * top wash gradient in .fm-sheet's own background instead, so the glow is one
 * continuous surface reaching the rounded top corners. */
.fm-sheet--concert   { --m13-edge-tone: var(--m13-edge-teal); }
.fm-sheet--online    { --m13-edge-tone: var(--accent-online); }   /* azure — M12.X-DETAIL-POPUP supersede violet (online už nesdílí fialovou s momenty); Petřin pokyn 2026-05-28 sjednoť mobil s desktop popupy */
.fm-sheet--poi       { --m13-edge-tone: var(--m13-edge-gold); }
.fm-sheet--gathering { --m13-edge-tone: var(--m13-edge-plum); }
/* Moment = the "mine" world (magenta), distinct from gathering's plum/iris so a
 * single moment never reads as a grouping (Petřin pokyn 2026-05-27). */
.fm-sheet--moment    { --m13-edge-tone: var(--m13-edge-magenta); }
.fm-sheet--profile   { --m13-edge-tone: var(--m13-edge-magenta); }

/* ── 11. .fm-sheet__chrome — top row: back · grip · spacer ────
 * M12D-4 (Petřin pokyn 2026-05-27): the × close button is gone (grip +
 * pull-to-dismiss closes the sheet). When opened from the list / a parent
 * gathering the left cell holds a back arrow; otherwise it's an empty spacer
 * that keeps the grip optically centred. */
.fm-sheet__chrome {
  display: flex;
  align-items: center;
  flex-shrink: 0;
  padding: 0 10px;
}
/* Cells stay symmetric (back arrow left, spacer/kebab right) so the grip
 * keeps its optical centre — both follow the 44px touch floor below. */
.fm-sheet__chrome-cell { width: 44px; flex-shrink: 0; }
.fm-sheet__back {
  /* Petřin pokyn 2026-05-28 "kulatou šipku vyhodit" — no boxy circle.
     Just the ghost arrow glyph with the atmospheric teal tone (matches
     "Back to Map" pattern). 44px tap floor — M13 review MOB-002 (the old
     36px values silently undercut the floor this comment promised). */
  min-width: 44px;
  height: 44px;
  flex-shrink: 0;
  display: grid;
  place-items: center;
  background: transparent;
  border: 0;
  color: var(--accent-turquoise);
  cursor: pointer;
  transition: color 160ms;
}
.fm-sheet__back:hover,
.fm-sheet__back:focus-visible {
  color: var(--accent-turquoise-bright);
  outline: none;
}

.fm-sheet__handle {
  flex: 1;
  /* Tighter grip band (Petřin pokyn 2026-05-29 — nahoře byl prázdný pruh,
     obsah má jít až nahoru k zaobleným rohům jako desktop panel). */
  padding: 6px 0 4px;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 6px;
  touch-action: none;
  cursor: grab;
  user-select: none;
}

.fm-sheet__handle:active { cursor: grabbing; }

/* The grip is a focusable slider (role=slider, arrow keys resize the sheet).
 * Without this the browser painted its DEFAULT focus ring as a wide rectangle
 * AROUND the whole flex:1 handle band as soon as the grip took focus on a tap
 * — Petra 2026-05-30 "bílé čáry kolem gripu". Drop the box outline; keep a
 * CONTAINED keyboard cue by brightening the grip bar itself (touch/pointer
 * focus → nothing; keyboard focus → the bar lights up, no rectangle). */
.fm-sheet__handle:focus { outline: none; }
.fm-sheet__handle:focus-visible .fm-sheet__handle-bar {
  background: rgba(238, 240, 250, 0.53);
  box-shadow: 0 0 0 4px rgba(238, 240, 250, 0.10);
}

.fm-sheet__handle-bar {
  width: 40px;
  height: 4px;
  border-radius: 2px;
  background: color-mix(in srgb, var(--text-light-muted) 33%, transparent);
  transition: background 200ms;
  /* M13.X-CHROME-REFLOW — the bar is the visible grip target; without its own
     touch-action a touch landing exactly on it could be claimed for body-scroll
     on the tall (scrollable) sheets, so mirror the handle's none. */
  touch-action: none;
}

.fm-sheet__handle:active .fm-sheet__handle-bar {
  background: rgba(238, 240, 250, 0.53);
}

.fm-sheet__body {
  flex: 1;
  min-height: 0;
  overflow-y: auto;
  padding: 0 16px 20px;
  -webkit-overflow-scrolling: touch;
  /* M12.X-GATHERING-POPUP (Petřin pokyn 2026-05-31) — scroll without a visible
     scrollbar lane (like the side feed); also keeps a desktop-narrow scrollbar from
     eating width + shifting echo dates out of alignment with the root date. */
  scrollbar-width: none;
}
.fm-sheet__body::-webkit-scrollbar { width: 0; height: 0; display: none; }

/* M12D-4 — the × close button was removed (the grip + pull-to-dismiss is the
 * close). With it gone, the old 40px top-right gutter that every header had to
 * reserve is gone too — that reclaims the dead band Petra flagged on the
 * gathering sheet (2026-05-27). Headers now run full width. */
.fm-sheet__body { padding-top: 2px; }

/* ── Backdrop — dim layer behind sheet (opacity set inline) ── */
.fm-sheet-backdrop {
  position: absolute;
  inset: 0;
  bottom: var(--m13-tab-bar-h);
  background: var(--m13-backdrop-color);
  /* opacity set inline by JS: 0 at peek → 0.45 at full */
  pointer-events: none;
  transition: background 220ms ease;
  z-index: var(--m13-z-backdrop);
}

/* Petřin pokyn 2026-05-28 "při otevřeném detailu se nemůžu hýbat po
 * mapě" — backdrop is now visual-only on every snap. Drag / pan / zoom
 * on the map underneath stays free; the user closes the sheet by
 * dragging the handle down (the engine's onDismiss path) or via the
 * sheet's own back/close affordance. The `--interactive` modifier is
 * kept defined as a no-op so the JS state toggle stays a 2-state
 * indicator (useful for backdrop-aware UI) but no longer blocks
 * pointer events.
 *
 * Tap-to-collapse on the dim is still wired in JS, but won't fire
 * because the backdrop no longer captures clicks. That trade is
 * intentional: a panning map is a higher-frequency UX than a dim-tap
 * close (which most users discovered by accident anyway). */
.fm-sheet-backdrop--interactive { pointer-events: none; }

/* ── 12. .fm-peek-header — common header inside every sheet ───
 * icon · title + subtitle + meta. Always shown (visible in peek).
 *
 * NOTE: handoff wrote `var(--m13-edge-tone)33` (token + bare hex
 * alpha suffix) which is INVALID CSS — a var() cannot be string-
 * concatenated with an alpha suffix. Corrected to color-mix() so
 * the tone tint actually renders. */
.fm-peek-header {
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 0 4px 12px;
}

.fm-peek-header__icon {
  width: 44px;
  height: 44px;
  border-radius: 12px;
  flex-shrink: 0;
  display: grid;
  place-items: center;
  background: linear-gradient(180deg,
    color-mix(in srgb, var(--m13-edge-tone, var(--accent-turquoise)) 20%, transparent),
    color-mix(in srgb, var(--m13-edge-tone, var(--accent-turquoise)) 7%, transparent));
  border: 1px solid color-mix(in srgb, var(--m13-edge-tone, var(--accent-turquoise)) 33%, transparent);
  box-shadow: inset 0 0 16px color-mix(in srgb, var(--m13-edge-tone, var(--accent-turquoise)) 20%, transparent);
  color: var(--m13-edge-tone, var(--accent-turquoise));
}

/* Centered, icon-less variant — reference sheets with no entity identity
 * (Legend). Title sits centred like the fullscreen-form head ("Leave an
 * Echo"), Petřin pokyn 2026-06-10: žádná ikonová dlaždice, nadpis uprostřed.
 * Size = the content-modal title tier (--fs-modal-title / 700, same as
 * #pin-form h2 per M13.X-TITLE-TIERS) — the 18px peek size equals the
 * legend ROW names, so the title didn't read as a title above them. */
.fm-peek-header--centered {
  justify-content: center;
}
.fm-peek-header--centered .fm-peek-header__text {
  flex: 0 1 auto;
}
.fm-peek-header--centered .fm-peek-header__title {
  text-align: center;
  font-size: var(--fs-modal-title);
  font-weight: 700;
  letter-spacing: 0.025em;
}

.fm-peek-header__title {
  font-family: var(--font-display);
  font-style: italic;
  font-weight: 600;
  font-size: 18px;
  line-height: 1.15;
  color: var(--text-dark);
  text-overflow: ellipsis;
  overflow: hidden;
  white-space: nowrap;
  margin: 0;
}

.fm-peek-header__subtitle {
  font: 500 11px var(--font-body);
  letter-spacing: 0.06em;
  color: var(--text-muted);
  margin-top: 3px;
  text-overflow: ellipsis;
  overflow: hidden;
  white-space: nowrap;
}
/* Concert location → soft-gold location tone (--location-color), parity with the
 * desktop popup `.popup--concert .popup__location` (M12D.X-EVENT-DETAIL-
 * PARITY §4). Online keeps --text-muted (its subtitle is the platform label,
 * not a venue), matching the base popup location tone. */
.fm-sheet--concert .fm-peek-header__subtitle { color: var(--location-color); }
/* Concert location + date → match the Concerts list row exactly (Petřin pokyn
 * 2026-05-31 "písmo má být stejně velké jako v seznamu"). The pixel size was
 * already 11px on both, but the list location is bold (--fw-bold) with --ls-
 * wider tracking (.feed-list__where) and the list date carries --ls-wide
 * (.feed-list__when); the detail header used 500 + 0.06em / no tracking, so it
 * read lighter ("smaller, different font"). Adopt the list tokens so detail
 * and list are one type system. Concert-scoped — online keeps the lighter
 * platform-label subtitle. */
.fm-sheet--concert .fm-peek-header__subtitle {
  font-weight: var(--fw-bold);
  letter-spacing: var(--ls-wider);
}
.fm-sheet--concert .fm-peek-header__meta { letter-spacing: var(--ls-wide); }

.fm-peek-header__text { flex: 1; min-width: 0; }

/* Date meta → muted (NOT the entity teal). Parity with the desktop popup
 * `.popup__date` = --text-muted (secondary metadata is intentionally dimmed,
 * not entity-accented). M12D.X-EVENT-DETAIL-PARITY §4. Shared across all
 * sheets (gathering/moment/POI date metas) — the muted treatment is the
 * canonical "date is secondary" cue everywhere. */
.fm-peek-header__meta {
  font: 500 11px var(--font-body);
  color: var(--text-muted);
  margin-top: 4px;
  display: flex;
  align-items: center;
  gap: 6px;
}

/* Header kebab "⋮" — Petřin pokyn 2026-05-28 "nemá to být tlačítkový
 * kebab, všude máme standardní kebab". Sjednoceno s .fm-moment-row__more
 * + .feed-list__row-menu-btn (panel rows): just the 3-dot glyph, no
 * box / no border / no bg — only currentColor on the SVG. Same 44px
 * tap floor for touch ergonomics. */
.fm-peek-header__kebab {
  flex-shrink: 0;
  align-self: flex-start;
  width: 44px;
  height: 44px;
  display: grid;
  place-items: center;
  background: transparent;
  border: 0;
  color: var(--text-light-muted);
  cursor: pointer;
  transition: color 140ms ease;
}
.fm-peek-header__kebab:hover,
.fm-peek-header__kebab:focus-visible {
  color: var(--text-dark);
  outline: none;
}

/* ── 13. .fm-attendance — avatar stack + counts ──────────────
 * Same shape in Concert + Online detail. Tap → list of people. */
.fm-attendance {
  /* M12.X-RSVP-UNIFY (Petřin pokyn 2026-05-30) — compact, NON-interactive summary
     (avatars are supplementary, not the main content). Container shrunk (NOT the
     text/numbers); chevron removed; matches the desktop popup .rsvp__counts tile. */
  display: flex;
  align-items: center;
  gap: 9px;
  padding: 6px 11px;
  border-radius: 10px;
  background: rgba(238, 240, 250, 0.04);
  border: 1px solid rgba(238, 240, 250, 0.08);
  color: var(--text-dark);
  width: 100%;
}

.fm-attendance__avatars {
  display: flex;
  flex-shrink: 0;
}

.fm-attendance__avatar {
  /* Small supplementary social proof (Petřin pokyn 2026-05-30 — "klidně výrazně
     menší"). 18px = same as the desktop popup avatar stack. */
  width: 18px;
  height: 18px;
  border-radius: 50%;
  margin-left: -7px;
  border: 1.5px solid;   /* bg + ring colour set inline per mood/user */
}
.fm-attendance__avatar:first-child { margin-left: 0; }

.fm-attendance__counts {
  flex: 1;
  min-width: 0;
  display: flex;
  /* Fidelity audit revision 2026-05-28 — mockup online_event.png actually
     WRAPS counts to two rows ("412 watching · 1290 interested" / "88
     watched") when they don't fit on one. The earlier nowrap pass
     truncated counts mid-word ("412 wa..."), which was worse than wrap.
     Reverted to wrap; each span stays nowrap individually so words
     themselves don't break mid-character. */
  flex-wrap: wrap;
  gap: 4px 10px;
  font: 500 12px var(--font-body);
  color: var(--text-muted);
}
.fm-attendance__counts > span {
  white-space: nowrap;
}

.fm-attendance__count-n {
  font: 700 14px var(--font-body);
  /* colour set inline per-state: --accent-violet (going/watching),
     --text-muted (interested), --text-light-muted (was there) */
}

/* ── 14. .fm-rsvp — Going / Interested (sibling pair) ────────
 * Both buttons share SHAPE. Selected = iris filled gradient;
 * unselected = iris-tinted outline. NEVER primary-vs-ghost. */
.fm-rsvp {
  display: flex;
  gap: 8px;
  /* M12.X-RSVP-UNIFY — 12px breathing room above the RSVP button (was 4px, too
     glued to the attendance summary). Matches desktop popup counts→CTA gap. */
  padding: 12px 0 14px;
}

.fm-rsvp__btn {
  flex: 1;
  /* M12.X-RSVP-UNIFY (Petřin pokyn 2026-05-30) — MATCHES the desktop popup
     .popup__cta reference EXACTLY (popup is the reference, mobile follows it):
     height via padding 0.5rem 1.1rem + --fs-sm (≈34px), --radius-sm. INACTIVE =
     NEUTRAL ghost outline (no teal/azure glow — accent must not read as selected);
     --selected = filled vertical gradient + glow + dark ink + ✓ (tone = sheet's
     --m13-edge-tone: concert teal / online azure). */
  padding: 0.5rem 1.1rem;
  border-radius: var(--radius-sm);
  font-family: var(--font-body);
  font-size: var(--fs-sm);
  font-weight: var(--fw-bold);
  letter-spacing: 0.02em;
  cursor: pointer;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 6px;
  /* INACTIVE = calm but CLEARLY a button (Petřin pokyn 2026-05-31) — subtle
     neutral surface + visible border + near-white label; accent only on selected. */
  background: rgba(238, 240, 250, 0.07);
  color: var(--text-light);
  border: 1px solid rgba(238, 240, 250, 0.30);
  transition: transform 200ms ease, box-shadow 200ms ease, background 180ms ease, color 180ms ease;
}

.fm-rsvp__btn--selected {
  /* Selected = filled vertical gradient + dark ink + glow shadow + the
     JS-injected "✓" suffix. Mirrors `.popup__cta.is-active`. Pinned TEAL (was
     the sheet's --m13-edge-tone, azure on online sheets) — attendance confirm
     is teal on BOTH event worlds, desktop + mobile (Petřin pokyn 2026-06-03,
     HARD RULE 12; ui-standards "ATTENDANCE CONFIRM = TEAL VŠUDE"). The sheet
     chrome keeps its per-world edge tone. */
  background: linear-gradient(180deg,
    color-mix(in srgb, var(--accent-turquoise) 90%, white 10%) 0%,
    color-mix(in srgb, var(--accent-turquoise) 70%, black) 100%);
  color: var(--surface-ink);
  box-shadow:
    0 4px 14px color-mix(in srgb, var(--accent-turquoise) 42%, transparent),
    inset 0 1px 0 rgba(238, 240, 250, 0.34);
}
/* Active primary CTA check suffix — canon `.popup__cta.is-active::after`
 * (same shape, mobile renders it via a JS-injected span instead of a
 * pseudo-element since the button label changes between event types). */
.fm-rsvp__btn-check {
  margin-left: var(--space-1);
  font-weight: var(--fw-bold);
}

.fm-rsvp__save {
  /* M12.X-RSVP-UNIFY — same heart glyph (canonical popup path, swapped in JS)
     + same box as .popup__cta-row .popup__save reference: 48 wide, height matches
     the 34px attendance button so the row is even (desktop ↔ mobile).
     NOTE (M13Q): this is the POI wishlist heart now — the EVENT like heart moved
     to the frameless counter `.fm-rsvp__like` below. */
  flex: 0 0 auto;
  width: 48px;
  height: 34px;
  padding: 0;
  border-radius: var(--radius-sm);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  background: transparent;
  border: 1px solid var(--border-on-light);
  color: var(--text-muted);
  cursor: pointer;
  flex-shrink: 0;
  transition: color 140ms ease, border-color 140ms ease;
}
.fm-rsvp__save:hover {
  color: var(--accent-magenta);
  background: transparent;
  border-color: rgba(var(--accent-magenta-rgb), 0.45);
}
.fm-rsvp__save:focus-visible {
  color: var(--accent-magenta);
  background: transparent;
  outline: 2px solid var(--accent-magenta);
  outline-offset: 2px;
}

/* M13Q — EVENT like heart = frameless COUNTER (heart glyph + count), INDEPENDENT
   of "I'm going" (Petřin pokyn 2026-06-01: no frame, just the counter). Distinct
   class from .fm-rsvp__save so the POI wishlist heart (above) stays a framed box.
   Same heart glyph + magenta + #heart-shade as the desktop popup .popup__save. */
.fm-rsvp__like {
  flex: 0 0 auto;
  flex-shrink: 0;
  display: inline-flex;
  align-items: center;
  gap: 5px;
  height: 34px;
  padding: 0 2px;
  border: none;
  background: transparent;
  color: var(--text-muted);
  cursor: pointer;
  transition: color 140ms ease, transform 140ms ease;
}
.fm-rsvp__like-glyph { display: inline-flex; align-items: center; }
.fm-rsvp__like svg { width: 26px; height: 26px; }
.fm-rsvp__like svg path { stroke-width: 1.4; }
.fm-rsvp__like-count {
  font: var(--fw-bold) 14px/1 var(--font-body);
  font-variant-numeric: tabular-nums;
  color: var(--text-muted);
}
.fm-rsvp__like:hover { color: var(--accent-magenta); }
.fm-rsvp__like:hover .fm-rsvp__like-count { color: var(--magentaLift); }
.fm-rsvp__like:active { transform: scale(0.92); }
.fm-rsvp__like:focus-visible {
  color: var(--accent-magenta);
  outline: 2px solid var(--accent-magenta);
  outline-offset: 2px;
  border-radius: var(--radius-sm);
}
.fm-rsvp__like.is-liked { color: var(--accent-magenta); }
.fm-rsvp__like.is-liked .fm-rsvp__like-count { color: var(--magentaLift); }
.fm-rsvp__like.is-liked svg path {
  fill: url(#heart-shade);
  stroke: var(--accent-magenta);
}

/* ── 15. (Removed ADR-255: dead `.fm-discussion-link` block — the mobile
 * sheets reuse the desktop renderDiscussionLinks() + `.popup__link`
 * visuals since M12D.X-EVENT-DETAIL-PARITY; nothing built this class.) */

/* =============================================================
 * MO-3 — DETAIL SHEET CONTENT (concert / online / poi / gathering /
 * profile / moment). Lives inside .fm-sheet__body, built by
 * mobileDetailSheets.js by REUSING the desktop detail data + the
 * shared rsvp.js control. Everything below is content styling only;
 * the sheet shell + tone edge + peek-header/rsvp/attendance/
 * discussion primitives are sections 9–15 above. Tokens only — no
 * hardcoded hex (CLAUDE.md UI standards).
 * ============================================================= */

/* Status-chip row above the peek header (tour name + state pill). */
.fm-status-chips {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 6px;
  padding: 2px 4px 10px;
}
.fm-status-chip {
  font: 700 10px var(--font-body);
  letter-spacing: 0.16em;
  text-transform: uppercase;
  padding: 4px 10px;
  border-radius: 999px;
  line-height: 1;
}
.fm-status-chip--tour {
  color: var(--accent-violet-soft);
  background: rgba(var(--accent-violet-rgb), 0.12);
  border: 1px solid rgba(var(--accent-violet-rgb), 0.40);
}
.fm-status-chip--standalone {
  color: var(--text-muted);
  background: rgba(238, 240, 250, 0.05);
  border: 1px solid var(--border-on-light);
}
.fm-status-chip--upcoming {
  color: var(--accent-copper);
  background: rgba(var(--accent-copper-rgb), 0.12);
  border: 1px solid rgba(var(--accent-copper-rgb), 0.40);
}
.fm-status-chip--past {
  color: var(--text-light-muted);
  background: rgba(238, 240, 250, 0.05);
  border: 1px solid var(--border-on-light);
}
.fm-status-chip--cancelled {
  color: var(--danger-text);
  background: rgba(var(--danger-rgb), 0.12);
  border: 1px solid var(--danger);
}
/* M12D.X-MODERATION-UNIFY (U7) — POI moderation status chips on the mobile sheet,
   tones mirrored from the desktop .poi-popup__status-chip (pending = warning,
   rejected = danger). Same fm-status-chip family so they sit with the others. */
.fm-status-chip--pending {
  color: var(--warning);
  background: rgba(var(--warning-rgb), 0.14);
  border: 1px solid rgba(var(--warning-rgb), 0.40);
}
.fm-status-chip--rejected {
  color: var(--danger-text);
  background: rgba(var(--danger-rgb), 0.12);
  border: 1px solid rgba(var(--danger-rgb), 0.36);
}

/* Cancelled banner — full-bleed rust bar above the chips. Mirrors the
 * desktop .event-detail__cancelled-banner (var(--cancelled-red)). */
.fm-cancelled-banner {
  display: flex;
  align-items: center;
  gap: 8px;
  background: var(--cancelled-red);
  color: var(--text-light);
  padding: 8px 12px;
  border-radius: 8px;
  font: 700 10px var(--font-body);
  letter-spacing: 0.20em;
  text-transform: uppercase;
  margin: 2px 0 12px;
}
.fm-cancelled-banner svg { flex-shrink: 0; }

/* Past treatment — archive cue WITHOUT dimming the content. A past event sheet
 * stays fully interactive + readable, so NO opacity/saturate on the content
 * (Petřin pokyn 2026-05-27 — whole-content dim was a bug; same fix as the desktop
 * popup). Past is signalled by the PAST status chip (.fm-status-chip--past). */
.fm-detail--past { /* no whole-content dim */ }

/* Content wrapper inside the sheet body — lets the Past treatment desaturate
 * everything below the chrome (handle/close) without touching the chrome. */
.fm-detail__content { display: block; }
.fm-gathering-moments { display: block; }

/* About / description paragraph. */
.fm-detail__about {
  font: 400 14px/1.5 var(--font-body);
  color: var(--text-muted);
  margin: 4px 0 14px;
}

/* More/Less inside the detail-sheet description takes the ENTITY tone —
 * same mechanism as the desktop popup (.popup--place sets --place-tone):
 * concert teal / online azure / landmark copper (Petřin pokyn 2026-06-05
 * „má být stejný jako na popupu"). Scoped to .fm-detail__about so the
 * gathering/moment toggles keep their own --label-color overrides. */
.fm-sheet--concert .fm-detail__about { --place-tone: var(--accent-turquoise); --place-tone-rgb: var(--accent-turquoise-rgb); }
.fm-sheet--online  .fm-detail__about { --place-tone: var(--accent-online);    --place-tone-rgb: var(--accent-online-rgb); }
.fm-sheet--poi     .fm-detail__about { --place-tone: var(--accent-copper);    --place-tone-rgb: var(--accent-copper-rgb); }
.fm-detail__about .text-expandable__toggle {
  color: var(--place-tone, var(--accent-violet-soft));
  text-decoration-color: rgba(var(--place-tone-rgb, var(--accent-violet-rgb)), 0.5);
}
.fm-detail__about .text-expandable__toggle:hover,
.fm-detail__about .text-expandable__toggle:focus-visible {
  color: var(--place-tone, var(--accent-violet-bright));
  text-decoration-color: currentColor;
}

/* Follow-up pills block (M12D.X-EVENT-DETAIL-PARITY) — the ticket/watch link
 * + community discussion links, rendered UNDER the attendance actions as one
 * light pill group (Petřin pokyn 2026-05-29). The pill markup is the desktop
 * popup's (`.popup__link` / `.popup__link--watch` / `.popup__discussion-*`),
 * whose CSS in style.css is unscoped and therefore styles the sheet verbatim.
 * This wrapper only owns spacing + the per-world watch-pill accent (the popup
 * tints the online watch pill via `.popup--online`, which the sheet markup
 * doesn't carry — so we re-apply azure here for online sheets).
 * Replaces the old standalone `.fm-detail__link` (now removed — the online
 * watch link became a pill in this block). */
.fm-followups {
  display: flex;
  flex-direction: column;
  gap: var(--space-1-5);
  margin: 2px 0 14px;
}
.fm-followups__discussion { display: block; }
/* Online watch pill → azure (parity with `.popup--online .popup__link--watch`).
 * Concert keeps the popup default teal. */
.fm-followups--online .popup__link--watch {
  background: color-mix(in srgb, var(--accent-online) 16%, transparent);
}
.fm-followups--online .popup__link--watch:hover {
  background: color-mix(in srgb, var(--accent-online) 26%, transparent);
}
.fm-followups--online .popup__link--watch .popup__link-ic {
  background: var(--accent-online);
  color: var(--text-on-accent);
}
/* "Add link" button stays TEAL even inside the online (azure) block — it's an
   ACTION (add a discussion link), so it wears the action/teal role, not the
   online world tone (Petřin bug 2026-05-31 + ui-standards HARD RULE 12; same
   call as the desktop .event-create__discussion-add). The base
   .popup__discussion-addbtn is already teal, so we simply DON'T override it
   here (the azure override was the bug). */
.fm-followups--online .popup__discussion-input:focus-visible {
  border-color: var(--accent-online);
}

/* Section divider with centered caps heading ("WHERE FANS ARE
 * TALKING", "5 MOMENTS"). */
.fm-section-head {
  display: flex;
  align-items: center;
  gap: 10px;
  margin: 14px 0 10px;
  font: 700 10px var(--font-body);
  letter-spacing: 0.20em;
  text-transform: uppercase;
  color: var(--text-light-muted);
}
.fm-section-head::before,
.fm-section-head::after {
  content: "";
  flex: 1;
  height: 1px;
  background: var(--border-on-light);
}

/* (Removed ADR-255: dead `.fm-discussion-link__icon/__body/__chevron`
 * remnants — see section 15 note above.) */

/* ── Cover strip — painterly cover image / tone placeholder (concert +
 * online detail, half/full). Mirrors desktop .event-detail__cover; the
 * placeholder gradient is tinted by the sheet's --m13-edge-tone so concert
 * reads teal + online reads iris (mockups 05/06/09). Tokens only. */
.fm-cover {
  position: relative; /* anchors the credit scrim overlaid on the photo */
  display: flex;
  flex-direction: column;
  align-items: center;
  margin: 2px 0 14px;
  border-radius: var(--radius-md);
  overflow: hidden;
  background: linear-gradient(135deg,
    color-mix(in srgb, var(--m13-edge-tone, var(--accent-turquoise)) 14%, transparent),
    color-mix(in srgb, var(--m13-edge-tone, var(--accent-turquoise)) 6%, transparent));
  border: 1px solid var(--border-on-light);
}
.fm-cover__img {
  display: block;
  width: 100%;
  /* Mobile detail = FIXED 3:2 frame the photo FILLS edge-to-edge
     (object-fit:cover) — every detail is the same height and the photo never
     looks letterboxed / "cut off at the top" (Petřin volba 2026-06-09 — „vyplnit
     pevný rámeček"). The editor focal point drives WHICH part stays in frame
     (object-position set inline in buildCover), so the author controls the crop
     here just like in the compact map popup hero. */
  aspect-ratio: 3 / 2;
  object-fit: cover;
}
.fm-cover--placeholder {
  /* No image → atmospheric painterly wash with a soft centre glow (mockup
   * 05). NO emoji glyph (mobile chrome canon: crisp surfaces, no glyphs).
   * Fidelity audit 2026-05-28 — earlier 14%/22% mix read as a near-black
   * box on the Noir surface; pumped to 22%/34% so the wash actually shows
   * the world's accent tone. */
  min-height: 130px;
  background:
    radial-gradient(ellipse 60% 70% at 50% 45%, color-mix(in srgb, var(--m13-edge-tone, var(--accent-turquoise)) 34%, transparent), transparent 70%),
    linear-gradient(135deg,
      color-mix(in srgb, var(--m13-edge-tone, var(--accent-turquoise)) 22%, transparent),
      color-mix(in srgb, var(--accent-violet) 10%, transparent));
}
/* Photo credit — small unobtrusive corner credit on the cover (Petřin volba
   2026-06-09 „malý text v rohu"; the full-width scrim band read as „strašně
   rušivé"). Compacted to author + license in JS. */
.fm-cover__credit {
  position: absolute;
  right: 8px;
  bottom: 6px;
  margin: 0;
  font: 500 10px var(--font-body);
  color: var(--text-light-muted);
  text-align: right;
  /* The mobile cover is a large bright photo where even a strong shadow gets
     lost, so the credit rides a SMALL frosted pill (hugs the text, NOT a
     full-width band — that was rejected as rušivé). Desktop hero stays
     text-only on purpose (Petřin pokyn 2026-06-09 — „na mobilu špatné, ale
     desktop už nezvýrazňovat víc"). */
  display: inline-block;
  max-width: calc(100% - 16px);
  padding: 2px 7px;
  border-radius: 999px;
  background: rgba(var(--cream-base-rgb), 0.55);
  -webkit-backdrop-filter: blur(2px);
  backdrop-filter: blur(2px);
  text-shadow: 0 1px 2px rgba(var(--cream-base-rgb), 0.85);
  pointer-events: none;
}

/* Active heart Save state — canonicalised with .popup__cta-row .popup__save.is-active
 * Petřin pokyn 2026-05-28 "to srdíčko už máme hezky vyřešené v popup systému".
 * Clean --accent-magenta (NOT --accent-mine — distinct deeper tone shared
 * across popup + marker dots; mine is a lighter pink for own-moment ring).
 * Frame stays, NO bg tile, NO glow. */
.fm-rsvp__save--active {
  background: transparent;
  border-color: rgba(var(--accent-magenta-rgb), 0.55);
  color: var(--accent-magenta);
}
.fm-rsvp__save--active svg path {
  /* Filled magenta heart so the favourite reads at a glance. The popup uses the
     same #heart-shade gradient (defined width=0 in index.html); mobile honours
     the same reference so the heart depth matches. */
  fill: url(#heart-shade);
  stroke: var(--accent-magenta);
}

/* ── Gathering sheet — 3-tier header (custom, NOT .fm-peek-header).
 * L1 name (Cormorant), L2 @author (magenta=mine / iris=other),
 * L3 caps location. Per m12-popups.md §2 + m13-detail-screens.md §4. */
.fm-gathering-header {
  display: flex;
  align-items: flex-start;    /* avatar + title + actions top-aligned (Petřin pokyn 2026-05-29 — kebab „plaval" při centrování proti 3řádkovému textu) */
  gap: 12px;
  padding: 0 0 12px;
}
/* M12D-4 — header-right action cluster (Add pebble + kebab/report). */
.fm-gathering-header__actions {
  display: flex;
  align-items: center;
  gap: 4px;
  flex-shrink: 0;
  align-self: flex-start;     /* sit by the title/OPEN row (gathering-level actions) */
}
/* M13.X-GATHERING-POLISH (Petřin bug 2026-06-01) — the kebab/report glyph must sit
   at the GATHERING-NAME level (mockup-v1-mobile.png), not floating down by the nick.
   The button keeps its 44px tap target, but .fm-sheet-action centres its glyph
   (place-items:center) → the dots landed at the button's mid-height (~name+nick).
   Top-align JUST the glyph so the dots ride the name line; tap area is unchanged. */
.fm-gathering-header__actions .fm-sheet-action {
  align-items: start;
}
/* Add / Join action — ONE light outlined pattern (Petřin pokyn 2026-05-29
 * "sjednotit detaily se seznamy: lehčí outline varianta, border, jemný
 * glow, ne tak těžké"). Dropped the heavy solid gradient pebble for a
 * low-tint fill + 1px tinted border + a subtle magenta glow so the action
 * reads lighter and consistent with the list/ghost affordances. Magenta =
 * the Moments/Gatherings world tone. */
.fm-gathering-add {
  height: 40px;
  min-width: 40px;
  padding: 0;
  border-radius: 50%;
  flex-shrink: 0;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 6px;
  /* Depth, not flat (Petřin pokyn 2026-05-29 — plochý prstenec vypadal mrtvě;
     seznamy jsou širší ale mají objem). Vertical tint gradient + top inset
     highlight + soft drop shadow = a raised, dimensional pill that stays
     light (low-tint fill, magenta tone), mirroring the .pin-popup__join lift. */
  background: linear-gradient(180deg,
    rgba(var(--accent-mine-rgb), 0.24) 0%, rgba(var(--accent-mine-rgb), 0.08) 100%);
  border: 1px solid rgba(var(--accent-mine-rgb), 0.55);
  color: var(--accent-mine);
  cursor: pointer;
  box-shadow:
    inset 0 1px 0 rgba(var(--moon-rgb, 238, 240, 250), 0.18),
    0 2px 6px rgba(0, 0, 0, 0.30),
    0 0 10px rgba(var(--accent-mine-rgb), 0.14);
  transition: background 140ms ease, box-shadow 140ms ease, transform 140ms ease;
}
.fm-gathering-add:hover,
.fm-gathering-add:focus-visible {
  background: linear-gradient(180deg,
    rgba(var(--accent-mine-rgb), 0.32) 0%, rgba(var(--accent-mine-rgb), 0.12) 100%);
  box-shadow:
    inset 0 1px 0 rgba(var(--moon-rgb, 238, 240, 250), 0.22),
    0 3px 8px rgba(0, 0, 0, 0.34),
    0 0 14px rgba(var(--accent-mine-rgb), 0.22);
  outline: none;
}
.fm-gathering-add:active { transform: translateY(1px); }
.fm-gathering-add svg { width: 20px; height: 20px; }

/* "Join" pill — non-owner on an OPEN gathering. Same light outlined family,
 * widened to a pill with the word "Join" so the action is self-explanatory
 * (Petřin pokyn 2026-05-29 — "pilulka s textem Join, ať je jasné, co dělá").
 * The owner's add stays a round "+" (no label). */
.fm-gathering-add--join {
  min-width: 0;
  border-radius: var(--radius-pill);
  padding: 0 13px 0 10px;
}
.fm-gathering-add__label {
  font: 700 12px var(--font-body);
  letter-spacing: 0.04em;
}
.fm-gathering-header__avatar {
  width: 48px;
  height: 48px;
  border-radius: 50%;
  flex-shrink: 0;
  /* Bare orb — per-avatar glow comes from the shared "Avatar unification" rule
     (style.css, ADR-192). NO ring/plate (Petřin pokyn 2026-05-30: avatar má mít
     jen záři jako v seznamu, žádný jiný styl kolem avataru není povolen). */
}
.fm-gathering-header__text { min-width: 0; flex: 1; }
/* Title row — gathering name + OPEN status badge side by side. OPEN is a
 * STATE of the gathering, so it belongs next to the name, NOT in the author
 * line (Petřin pokyn 2026-05-29). The name shrinks/ellipsises so the badge
 * always stays visible. */
.fm-gathering-header__title-row {
  display: flex;
  align-items: center;
  gap: 8px;
  min-width: 0;
}
.fm-gathering-header__name {
  font-family: var(--font-display);
  font-style: italic;
  font-weight: 600;
  font-size: 19px;
  /* Tighter leading so the name→nick→location stack reads compact, not spread
     (Petřin pokyn 2026-06-01). */
  line-height: 1.1;
  color: var(--text-dark);
  margin: 0;
  min-width: 0;
  flex: 0 1 auto;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
/* M12D.X-ADMIN-MOBILE-PARITY (bug #6) — "Open" status badge, mirroring the
   desktop .pin-popup__open-badge. One consistent teal open/live tone for every
   viewer (it's a STATUS, not an ownership signal). Tokens only. */
.fm-gathering-header__open-badge {
  display: inline-flex;
  align-items: center;
  flex-shrink: 0;             /* stays visible while the name ellipsises */
  padding: 1px 8px;
  border-radius: var(--radius-pill);
  font: 700 10px var(--font-body);
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--live);
  background: rgba(var(--live-rgb), 0.14);
  border: 1px solid rgba(var(--live-rgb), 0.34);
}
.fm-gathering-header__author {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  /* M13.X-GATHERING-POLISH (Petřin pokyn 2026-06-01, re-measured) — the FOUNDER
     nick reads as the prominent identity line through its MAGENTA tone + the ✦
     mark + the @, NOT a big size. The mockup founder nick is ~13px, same scale as
     the child @nicks; prominence is colour, not size. */
  font: 700 13px var(--font-body);
  letter-spacing: 0.01em;
  color: var(--accent-mine);
  margin-top: 3px;            /* small, deliberate rhythm — name→nick→location stack
                                 reads compact like the mockup (Petřin bug 2026-06-01;
                                 was a stale-CSS 24px gap on Petra's build) */
  background: none;
  border: 0;
  padding: 0;
  cursor: pointer;
}
.fm-gathering-header__author img {
  /* Petřin pokyn 2026-05-28 "vlajka u uživatele má být větší než vlajka
     u lokace, stejná logika jako v popupu" — user flag dominantní,
     mirror .feed-list__nationality vs .feed-list__flag--small ratio. */
  width: 18px;
  height: auto;
  border-radius: 2px;
}
.fm-gathering-header--other .fm-gathering-header__author {
  color: var(--nick-color);
}
.fm-gathering-header__location {
  display: flex;
  align-items: center;
  gap: 6px;
  font: 700 10px var(--font-body);
  letter-spacing: 0.18em;
  text-transform: uppercase;
  /* Match the LIST location token (.feed-list__item--pin .feed-list__where =
     --location-color, soft gold), not the generic --text-muted grey — so the
     gathering detail + list read as one system (Petřin bug 2026-05-31). */
  color: var(--location-color);
  margin-top: 3px;            /* match the nick rhythm — compact 3-line stack (Petřin bug 2026-06-01) */
  /* Fidelity audit 2026-05-28 — long region names ("LESSER POLAND
     VOIVODESHIP") wrapped to a second row when the action cluster
     (+ pebble + kebab/report) ate the remaining width. Mockup keeps the
     line on a single row; ellipsis when it must shrink. */
  min-width: 0;
}
.fm-gathering-header__location > :last-child {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  min-width: 0;
}
.fm-gathering-header__location img {
  /* Location flag = smaller per popup convention (Petřin pokyn 2026-05-28). */
  width: 13px;
  height: auto;
  border-radius: 2px;
}

/* M12D-4 (Petrin pokyn 2026-05-27) - sheet action row. Gathering = add + kebab;
 * moment = kebab. Ghost icons that match the panel-list row kebab
 * (.feed-list__row-menu-btn) so the sheet reads as the same product (the old
 * boxed .fm-icon-btn looked foreign). Tight + right-aligned (no dead band). */
.fm-sheet-action {
  width: 44px;
  height: 44px;
  border-radius: var(--radius-sm, 8px);
  display: grid;
  place-items: center;
  background: transparent;
  border: 0;
  color: var(--text-muted);
  cursor: pointer;
  transition: background 140ms ease, color 140ms ease;
}
.fm-sheet-action:hover,
.fm-sheet-action:focus-visible {
  background: rgba(238, 240, 250, 0.08);
  color: var(--text-dark);
  outline: none;
}
.fm-sheet-action--add { color: var(--accent-turquoise); }
.fm-sheet-action--danger { color: var(--danger-text); }
.fm-sheet-action--mine { color: var(--accent-mine); }
.fm-sheet-action:disabled { opacity: 0.4; cursor: default; }

/* M12D.X-REPORT-INDICATORS (bod 2/3 — Petřin pokyn 2026-05-30): LOUD reported
   state for a FLAG button (buildSheetIconBtn — e.g. the gathering Report icon).
   Solid danger fill + halo + dark glyph = a clearly flagged flag, same treatment
   as .pin-popup__report.is-reported on desktop. NOTE: this is applied ONLY to a
   flag glyph — the kebab (⋮) must NOT get the solid fill (three dots on a red
   block read as a broken button), it uses the flag BADGE below instead. */
.fm-sheet-action.is-reported {
  color: var(--text-on-accent);
  background: var(--danger);
  box-shadow: var(--halo-danger);
}
.fm-sheet-action.is-reported:hover,
.fm-sheet-action.is-reported:focus-visible {
  color: var(--text-on-accent);
  background: var(--danger-bright);
  box-shadow: var(--halo-danger);
}

/* Reported KEBAB (Petřin pokyn 2026-05-31) — JUST the dots go danger-red. NO
   badge, NO box/ring/fill around the kebab (the square read as a broken control,
   Petra "nechci čtvereček kolem kebabu"). The flag lives as the highlighted
   "Moderate" row INSIDE the opened sheet (.fm-action-sheet__item--reported).
   Keep the red on hover too (base :hover would otherwise relight the dots). */
.fm-sheet-action--has-report,
.fm-moment-row__more--has-report,
.fm-sheet-action--has-report:hover,
.fm-sheet-action--has-report:focus-visible,
.fm-moment-row__more--has-report:hover,
.fm-moment-row__more--has-report:focus-visible {
  color: var(--danger-text);
}

/* .fm-icon-btn — boxed icon button, still used by the profile sheet kebab. */
.fm-icon-btn {
  width: 44px;
  height: 44px;
  border-radius: 12px;
  display: grid;
  place-items: center;
  background: rgba(238, 240, 250, 0.04);
  border: 1px solid var(--border-on-light);
  color: var(--text-light-muted);
  cursor: pointer;
}
.fm-icon-btn--add { color: var(--accent-turquoise); }
.fm-icon-btn--add:hover { border-color: rgba(var(--accent-turquoise-rgb), 0.45); }
/* M12D.X-REPORT-INDICATORS — Report = --danger (HARD RULE 3 / ui-standards §482),
   NEVER magenta. Magenta (--accent-mine) means "my own Echo/moment"; using it for
   a report flag was a token-role violation (was: color: var(--accent-mine)). */
.fm-icon-btn--report:hover { color: var(--danger-text); }
.fm-icon-btn--danger { color: var(--danger-text); }

/* Moment list row (gathering moments + profile moments). Teal note
 * glyph + name + right date + (placeholder) more dot. */
.fm-moment-row {
  /* M12.X-GATHERING-POPUP mobile parity (Petřin pokyn 2026-05-31) — grid so the
     DATE is its OWN full-width second row, right-aligned UNDER the message (not on
     the @nick line) and lined up with the root body's date — mirror of the desktop
     popup _renderPreviewRow. Row 1 = body | kebab; row 2 = the date spanning both,
     right-aligned. The kebab stays in its column, always visible. */
  display: grid;
  grid-template-columns: 1fr auto;
  grid-template-areas:
    "body kebab"
    "when when";
  column-gap: 10px;
  /* M13.X-GATHERING-POLISH (Petřin pokyn 2026-06-01, measured) — the like row
     sits in the `when` grid row; 6px (was 3px) gives the heart breathing room
     above it, matching the founding card. */
  row-gap: 6px;
  align-items: start;
  padding: 10px 8px;
  border-radius: 9px;
  width: 100%;
  text-align: left;
  background: transparent;
  border: 0;
  border-bottom: 1px solid var(--border-on-light);
  color: var(--text-dark);
  cursor: pointer;
}
.fm-moment-row:last-child { border-bottom: 0; }
/* Tighter top inside gathering echo rows (Petřin bug 2026-05-31 — moc prázdna
   nad usernamem). Scoped to the gathering list so profile moment rows keep their
   rhythm. */
.fm-gathering-moments .fm-moment-row { padding-top: 5px; padding-bottom: 6px; }
/* Open-gathering rows lead with the author avatar → 3 columns. The date row
   leaves the avatar gutter empty and spans body+kebab, right-aligned. */
.fm-moment-row--lead {
  grid-template-columns: auto 1fr auto;
  grid-template-areas:
    "avatar body kebab"
    ".      when when";
}
.fm-moment-row__glyph { grid-area: avatar; }
.fm-moment-row__body { grid-area: body; }
/* M17.X-KEBAB-NAME-LEVEL (Petřin pokyn 2026-06-09, parity s desktop popupem) —
   kebab at the @nick line, not centred on the body row. Re-flips the 2026-06-03
   centring back to top-align: Petra wants the dots at the NAME level on every
   surface. On a tall (song-tagged) row the dots stay up by the @nick. */
.fm-moment-row > .fm-moment-row__more { grid-area: kebab; align-self: start; }
.fm-moment-row > .fm-moment-row__meta {
  /* Meta row — like control (left) + date (right). */
  grid-area: when;
  display: flex;
  align-items: center;
  gap: 8px;
}
.fm-moment-row__meta > .fm-moment-row__date {
  margin-left: auto;
  /* 2026-06-03 evening (Petřin pokyn, parity s desktop popupem) — gathering row
     date owns the full-width meta row; don't glue it to the sheet edge. Right
     edge tucks under the kebab DOTS above (⋮ 18px glyph centered in the 28px
     .fm-moment-row__more button → dots end ~13px from the row edge). The
     founding root date (.fm-gathering-root__date) lives in its own card meta
     row and keeps the card inset — this selector hits child rows only. */
  padding-right: 13px;
}
.fm-moment-row__glyph {
  width: 26px;
  height: 26px;
  border-radius: 7px;
  display: grid;
  place-items: center;
  flex-shrink: 0;
  background: rgba(var(--accent-turquoise-rgb), 0.13);
  border: 1px solid rgba(var(--accent-turquoise-rgb), 0.33);
  color: var(--accent-turquoise);
}
/* Avatar variant — Petřin pokyn 2026-05-28: open gathering shows the
 * contributor avatar instead of the generic note glyph. Drop the tile bg
 * + border so the avatar's own coloured ring + glow read cleanly.
 * NO overflow:hidden (Petřin pokyn 2026-06-01): the ADR-192 per-avatar
 * --av-glow drop-shadow (on the inner svg, style.css) extends ~7px past the
 * 26px orb box; clipping it swallows the halo, so the child-echo avatar in a
 * mobile gathering read flat vs feed/popup/detail. The svg is already a
 * circular orb (avatars.js), so there is nothing to clip. Mirrors desktop
 * .pin-popup__preview-avatar (no overflow wrapper). */
.fm-moment-row__glyph--avatar {
  background: transparent;
  border: 0;
  border-radius: 50%;
}
.fm-moment-row__glyph--avatar svg { width: 26px; height: 26px; }
/* Vertical body: expandable text + date stacked. Takes the row's flex space so
   the kebab stays pinned right; the text wraps/clamps inside (M12D.X-DETAIL-
   ACTIONS 2026-05-30 — mirrors the feed list body). */
.fm-moment-row__body {
  flex: 1;
  min-width: 0;
  display: flex;
  flex-direction: column;
  gap: 3px;
}
.fm-moment-row__name {
  min-width: 0;
  /* M12 (Petřin pokyn 2026-05-27) — moment voice (roman default; --hand short).
     Tokens only; values shared with desktop popup/feed. */
  font-family: var(--font-moment-long);
  font-size: var(--fs-moment-roman);
  font-weight: var(--fw-regular);
  line-height: var(--lh-moment-roman);
  color: var(--text-dark);
  /* No single-line nowrap/ellipsis here — the nested .text-expandable clamps
     the text to N lines + shows More/Less. nowrap was inherited by the
     expandable body, forcing one line → clamp never overflowed → toggle stayed
     hidden (Petřin pokyn 2026-05-30 "v gatheringu chybí less a more"). */
}
.fm-moment-row__name--hand {
  font-family: var(--font-moment);
  font-size: var(--fs-moment-hand);
  font-weight: var(--fw-moment-hand);
  line-height: var(--lh-moment-hand);
}
/* Mobile-only: gathering detail echo bodies match the echo detail / feed roman
   bump (0.82→0.9rem) for readability (Petřin pokyn 2026-06-08). Scoped to the
   gathering surfaces (founding root body + child echo rows) so profile moment
   rows keep their size; --hand (handwriting) voice unchanged. */
.fm-gathering-root .fm-moment-row__name:not(.fm-moment-row__name--hand),
.fm-gathering-moments .fm-moment-row__name:not(.fm-moment-row__name--hand) {
  font-size: 0.9rem;
}
.fm-moment-row__date {
  flex-shrink: 0;
  /* M13.X-GATHERING-POLISH (ADR-213) — CHILD echo date is deliberately QUIET
     (Manrope 500/11px, --text-light-muted), one step softer than the prominent
     root date, so the founding echo's date reads first (handoff). */
  font: 500 11px var(--font-body);
  letter-spacing: 0.03em;
  color: var(--text-light-muted);
}
/* M12.X-GATHERING-POPUP mobile parity (Petřin pokyn 2026-05-31) — the FOUNDING
   echo's comment is the gathering BODY (rendered once at the top, mirror of the
   desktop popup .pin-popup__root-text), with its date right-aligned below. Text
   reuses .fm-moment-row__name so the moment voice is one system across surfaces.
   M13.X-GATHERING-POLISH (ADR-213) — the root now lives in its OWN contained card
   (subtle light-ink wash + hairline + md radius) headed by the "✦ FOUNDING ECHO"
   eyebrow, mirror of the desktop popup (mockup-v1-mobile.png). */
.fm-gathering-root {
  display: flex;
  flex-direction: column;
  /* 4px (was 6px) — tighten eyebrow→comment→meta rhythm (Petřin pokyn 2026-06-01,
     measured: eyebrow sat too far from the comment). */
  gap: 4px;
  background: rgba(var(--text-light-rgb), 0.03);
  border: 1px solid var(--border-on-light);
  border-radius: var(--radius-md);
  padding: 11px 14px 12px;
}
.fm-gathering-root__eyebrow {
  display: flex;
  align-items: center;
  gap: 5px;
  /* Micro caps label — 9px (0.6rem), matching the ECHOES divider micro-label;
     small eyebrow, the founder nick in the header is the prominent line. */
  font-family: var(--font-body);
  font-weight: 700;
  font-size: 0.6rem;
  letter-spacing: 0.2em;
  text-transform: uppercase;
  color: var(--accent-mine);
}
/* M17.X-FOUNDING-OWNERSHIP (Petřin pokyn 2026-06-09) — magenta = "mine"; a
   foreign founder's eyebrow takes the foreign-nick violet (--nick-color), so
   the colour alone says whose gathering this is. Parity with desktop. */
.fm-gathering-root__eyebrow--other {
  color: var(--nick-color);
}
.fm-gathering-root__eyebrow::before {
  content: "\2726"; /* ✦ founder mark */
  font-size: 1.05em;
  line-height: 1;
}
/* M18.X-ECHO-DELETE-RULES — founding-echo tombstone notice ("Original echo
   was deleted"): no eyebrow, no like, just this italic line + the kept date.
   Mirror of the desktop .pin-popup__tombstone-text (same dedicated token —
   muted-but-readable, never --text-muted). */
.fm-gathering-root__tombstone {
  font-family: var(--font-body);
  font-style: italic;
  color: var(--text-tombstone);
}
/* Meta row under the founding body: LEFT slot reserved for the per-echo like
   (M13.X-ECHO-LIKES); date right-aligned via margin-left:auto. */
.fm-gathering-root__meta {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  /* M13.X-GATHERING-POLISH (Petřin pokyn 2026-06-01, measured) — more breathing
     room ABOVE the like row (was 2px, read cramped against the comment). */
  margin-top: 6px;
}
.fm-gathering-root__meta .fm-gathering-root__date {
  margin-left: auto;
}
/* Root date stays PROMINENT (Manrope 600/12px, --text-muted) — one step louder
   than the quiet child dates (handoff: the founder's date leads). */
.fm-gathering-root__date {
  font: 600 12px var(--font-body);
  color: var(--text-muted);
}
/* The gathering More/Less toggle uses the LABEL token (soft teal --label-color,
   same as the location caps), NOT the default violet (handoff fix). Scoped to the
   gathering surfaces only — the founding card + the echo rows — so other sheets'
   expandables keep their default tone. */
.fm-gathering-root .text-expandable__toggle,
.fm-gathering-moments .text-expandable__toggle {
  color: var(--label-color);
  text-decoration-color: color-mix(in srgb, var(--label-color) 50%, transparent);
  /* Slightly less top gap than the global 0.35rem (Petřin pokyn 2026-06-01). */
  margin-top: 0.25rem;
}
.fm-gathering-root .text-expandable__toggle:hover,
.fm-gathering-root .text-expandable__toggle:focus-visible,
.fm-gathering-moments .text-expandable__toggle:hover,
.fm-gathering-moments .text-expandable__toggle:focus-visible {
  color: var(--label-color);
  text-decoration-color: currentColor;
}
.fm-gathering-root__text--muted {
  color: var(--text-muted);
  font-style: italic;
}
/* ECHOES divider — NOT sticky on mobile (Petřin pokyn 2026-06-11, supersedes the
   2026-05-31 sticky parity): the transparent pinned head collided with the echo
   rows scrolling under it (no opaque bg allowed — it banded against the tone
   gradient, pokyn 2026-05-31 — so sticky + transparent can't coexist). The head
   now scrolls away with the list. Desktop popup keeps ITS sticky (style.css). */
.fm-gathering-moments .fm-section-head {
  /* Tighter band (Petřin bug 2026-05-31 — moc prázdna nad ECHOES oddělovačem +
     nad prvním usernamem pod ním). Was margin 14/10 + padding-block 8. */
  margin: 0 0 4px;
  padding-block: 4px;
  background: transparent;
}
/* M12.X-GATHERING-POPUP (Petřin pokyn 2026-05-31) — a little breathing room from the
   grip so the gathering header isn't jammed under the handle. */
.fm-sheet--gathering .fm-detail__content {
  padding-top: 10px;
}
/* Gathering chips (TEST / OPEN) align with the NAME, not the avatar's left edge
   (Petřin bug 2026-05-31). Inset = avatar width (48) + header gap (12). */
.fm-gathering-chips {
  padding-left: 60px;
}
/* The TEST pill carries a margin-left meant for INLINE placement after a name;
   as the first chip in its own row above the name it leaked ~14px and pushed the
   whole chip group right of the gathering name. Zero it so the chips align flush
   with the name's left edge (Petřin pokyn 2026-06-01 — chips nezarovnané). The
   TEST↔OPEN spacing comes from .fm-status-chips `gap`. */
.fm-gathering-chips .test-data-pill {
  margin-left: 0;
}
/* Sheet footer — pins a persistent CTA to the bottom of the sheet OUTSIDE the
   scrollable body (sibling of .fm-sheet__body in the flex column), so it stays put
   while the body scrolls (sticky-inside-body did NOT hold on device; Petřin pokyn
   2026-05-31). flex-shrink:0 keeps its height; the body (flex:1) gives up space to
   it. fitToContent (bottomSheet.js) adds its height to the sheet size. */
.fm-sheet__footer {
  flex-shrink: 0;
  padding: 12px 16px calc(12px + env(safe-area-inset-bottom, 0px));
  background: var(--cream-surface);
  border-top: 1px solid var(--border-on-light);
}
/* M12.X-GATHERING-POPUP (Petřin pokyn 2026-05-31) — primary CTA, full-width at the
   BOTTOM of the sheet (moved out of the crowded header). Visual = the concert
   "I'm going" button (.fm-rsvp__btn--selected) EXACTLY — same height
   (padding 0.5rem 1.1rem ≈34px), --radius-sm, --fs-sm/bold, filled vertical
   gradient + glow + dark ink — but MAGENTA (the echo/moment world owns magenta),
   not the concert teal/azure. */
.fm-gathering-cta {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 6px;
  /* Fills the footer (which supplies the side gutter + breathing). A <button>
     keeps intrinsic shrink-to-fit even as a flex container, so width is explicit. */
  width: 100%;
  box-sizing: border-box;
  margin: 0;
  padding: 0.5rem 1.1rem;
  border-radius: var(--radius-sm);
  border: 0;
  font-family: var(--font-body);
  font-size: var(--fs-sm);
  font-weight: var(--fw-bold);
  letter-spacing: 0.02em;
  cursor: pointer;
  background: linear-gradient(180deg,
    color-mix(in srgb, var(--accent-mine) 90%, white 10%) 0%,
    color-mix(in srgb, var(--accent-mine) 70%, black) 100%);
  color: var(--surface-ink);
  box-shadow:
    0 4px 14px color-mix(in srgb, var(--accent-mine) 42%, transparent),
    inset 0 1px 0 rgba(238, 240, 250, 0.34);
  transition: transform 200ms ease, box-shadow 200ms ease;
}
.fm-gathering-cta:active { transform: translateY(1px); }
.fm-gathering-cta svg { width: 18px; height: 18px; flex-shrink: 0; }
/* Anonymous sign-in nudge — link-look, not a loud button (anon nudge = link). */
.fm-gathering-cta--nudge {
  background: none;
  box-shadow: none;
  color: var(--accent-mine);
  text-decoration: underline;
  text-underline-offset: 3px;
  font-weight: var(--fw-regular);
}
/* M12.X (Petřin pokyn 2026-05-30) — open-gathering echo rows lead with an
   identity line: @nick + author flag (left) · date (right), ABOVE the text.
   Mirrors the desktop popup row + the feed list so the same echo reads the
   same on every surface. */
.fm-moment-row__identity {
  display: flex;
  align-items: center;
  gap: 6px;
  min-width: 0;
}
.fm-moment-row__author {
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  font: 600 12px var(--font-body);
  letter-spacing: 0.01em;
  color: var(--nick-color);
}
/* Viewer-relative tone + founder mark — mirror of the desktop popup
   (.pin-popup__preview-author--mine/--founder). The earlier "author is always
   another fan" note was wrong: a CLOSED gathering's children are the founder's
   own echoes (Petřina REGRESE 2026-06-03 — own child read violet, no ✦). Mine
   = colour only; ✦ = the FOUNDER's mark for every viewer, tone-independent. */
.fm-moment-row__author--mine {
  color: var(--accent-mine);
}
.fm-moment-row__author--founder::before {
  content: "✦";
  margin-right: 0.3rem;
  font-size: 0.65rem;
  vertical-align: 1px;
}
.fm-moment-row__author[role="button"] { cursor: pointer; }
.fm-moment-row__flag {
  flex-shrink: 0;
  width: 18px;
  height: 13px;
  border-radius: 2px;
  object-fit: cover;
}
/* (Removed: the identity-line date. The date now sits on its own full-width row
   below the message, right-aligned — see .fm-moment-row grid above. Petřin pokyn
   2026-05-31.) */
.fm-moment-row__more {
  /* Fidelity audit 2026-05-28 — flex-shrink:0 was already here, but the
     glyph SVG inside lacked a fixed width, so on the harness render the
     kebab dropped onto its own row. Re-state the size constraints
     explicitly so the button stays anchored to the right edge of the row
     even when the name uses up the remaining flex space. */
  flex-shrink: 0;
  width: 28px;
  height: 28px;
  min-width: 28px;
  border-radius: 8px;
  display: grid;
  /* 2026-06-03 evening (Petřin pokyn) — glyph centred in the button again; the
     2026-06-01 top-align is superseded together with the row-level centring
     (see the grid-area rule above): the button itself now centres on the body
     row, so the dots no longer need to chase the text line. */
  place-items: center;
  color: var(--text-light-muted);
  background: none;
  border: 0;
}
.fm-moment-row__more svg { flex-shrink: 0; }
/* .fm-moment-row__more--has-report (reported cue) is styled together with
   .fm-sheet-action--has-report above (highlighted danger kebab, Petřin pokyn
   2026-05-31). */
.fm-moment-empty {
  font: 400 13px var(--font-body);
  color: var(--text-muted);
  padding: 8px 4px;
}

/* A small footnote (proposal-screen disclaimer). */
.fm-detail__footnote {
  font: 400 11px/1.4 var(--font-body);
  color: var(--text-light-muted);
  margin: 14px 0 4px;
  padding-top: 10px;
  border-top: 1px solid var(--border-on-light);
}

/* Sheet-level loading / error states. */
.fm-detail__loading,
.fm-detail__error {
  font: 500 13px var(--font-body);
  color: var(--text-muted);
  padding: 18px 4px;
  text-align: center;
}

/* ── Moment detail sheet (M12D-4, Petřin pokyn 2026-05-27) — a single post,
 * NOT a grouping. Top: small author chip + kebab. Then a muted meta line.
 * Then the NOTE as the hero (moment voice). Visually distinct from the
 * gathering sheet (which leads with a big name + a moment list). */
.fm-moment-detail__top {
  display: flex;
  /* TOP-align: avatar + (@nick over location) text column + kebab all start at the
     top line, so the location stacks UNDER the nick like the gathering header
     (Petřin pokyn 2026-06-01), not vertically centred against the avatar. */
  align-items: flex-start;
  gap: 12px;
  padding: 0 0 10px;
}
/* @nick + location stack — mirrors .fm-gathering-header__text. */
.fm-moment-detail__textcol {
  display: flex;
  flex-direction: column;
  min-width: 0;
  flex: 1;
}
/* Status row — Open chip + add/join control on their OWN line below the
   author (M12D.X-DETAIL-ACTIONS, Petřin pokyn 2026-05-30: open+join utlačovaly
   nickname → druhý řádek). Left-aligned, small scale. */
.fm-moment-detail__status {
  display: flex;
  align-items: center;
  gap: 8px;
  flex-wrap: wrap;
  padding: 0 0 10px;
}
/* Compact the add/join control here so it sits at the OPEN chip's scale, not
   the 40px header pebble (Petřin pokyn 2026-05-30: "Join menší, ne o moc větší
   než open"). The base .fm-gathering-add geometry is overridden in this row
   only — the gathering header keeps its full-size pebble. */
.fm-moment-detail__status .fm-gathering-add {
  height: 32px;
  min-width: 32px;
}
.fm-moment-detail__status .fm-gathering-add svg { width: 16px; height: 16px; }
.fm-moment-detail__status .fm-gathering-add--join {
  padding: 0 12px 0 9px;
  gap: 4px;
}
.fm-moment-detail__status .fm-gathering-add__label {
  font: 700 12px var(--font-body);
  letter-spacing: 0.03em;
}
/* M12D-4 (Petřin pokyn 2026-05-28) — right-side action cluster on a
 * moment detail: + add another moment at this place (magenta pebble,
 * same as gathering) followed by the kebab (own) or the Report icon
 * (other). Mirrors .fm-gathering-header__actions so the two surfaces
 * read alike. */
.fm-moment-detail__actions {
  display: flex;
  align-items: center;
  gap: 4px;
  flex-shrink: 0;
}
.fm-moment-detail__author {
  display: inline-flex;
  align-items: center;
  gap: 9px;
  min-width: 0;
  align-self: flex-start;     /* shrink to the nick width (not full column) so only the nick is the profile tap target */
  background: none;
  border: 0;
  /* No vertical padding — keeps the nick→location stack tight (compact, no holes;
     Petřin pokyn 2026-06-01). The location's own 3px margin-top is the only gap. */
  padding: 0 4px 0 0;
  cursor: pointer;
  color: var(--accent-mine);
  /* M13.X (Petřin pokyn 2026-06-01) — the standalone Echo's nick matches the
     side-list nick (.feed-list__title) EXACTLY: Cormorant 700, 1.1rem. Only the
     STANDALONE echo grows; the gathering founder nick stays 13px (its prominence
     is the magenta + ✦, decided 2026-06-01). Tone (mine=magenta / other=violet)
     is unchanged — only the font family + size match the list. */
  font-family: var(--font-display);
  font-weight: 700;
  font-size: 1.1rem;
  letter-spacing: 0.02em;
  line-height: 1.2;
}
.fm-moment-detail__author--other { color: var(--nick-color); }
.fm-moment-detail__author:disabled { cursor: default; }
.fm-moment-detail__avatar {
  width: 34px;
  height: 34px;
  border-radius: 50%;
  flex-shrink: 0;
  /* Bare orb — per-avatar glow from the shared "Avatar unification" rule
     (style.css, ADR-192). NO ring/plate (Petřin pokyn 2026-05-30). */
}
.fm-moment-detail__nick {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  min-width: 0;
  flex: 0 1 auto;
}
/* Nick + nationality flag grouped tight (flag right after nick — list parity,
   M12D.X-DETAIL-ACTIONS 2026-05-30). The flag's own margin-left gives the gap;
   the group (not the bare nick) ellipsises. */
.fm-moment-detail__nickgroup {
  display: inline-flex;
  align-items: center;
  min-width: 0;
}
.fm-moment-detail__meta {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 6px;
  font: 600 11px var(--font-body);
  letter-spacing: 0.04em;
  color: var(--text-muted);
  margin: 0 0 8px;
}
.fm-moment-detail__meta img { width: 16px; height: auto; border-radius: 2px; }
.fm-moment-detail__meta-sep { color: var(--text-light-muted); }
.fm-moment-detail__context {
  display: flex;
  align-items: center;
  gap: 6px;
  font: 600 11px var(--font-body);
  letter-spacing: 0.04em;
  color: var(--text-light-muted);
  margin: 0 0 12px;
}
.fm-moment-detail__context-mark { color: var(--accent-violet-soft); }
.fm-moment-detail__photo {
  width: 100%;
  max-height: 220px;
  object-fit: cover;
  border-radius: 12px;
  margin: 2px 0 12px;
  border: 1px solid var(--border-on-light);
}
/* HERO note — the user's own words in the moment voice (ADR-190 tokens; one
   system across popup/list/Yours/sheet — no bespoke per-window size). --hand =
   short moment (handwriting); default = roman serif for longer. The note wraps
   the shared text-expandable (More/Less), so the inner paragraphs inherit the
   voice. */
.fm-moment-detail__note {
  margin: 2px 0 6px;
  color: var(--text-dark);
  font-family: var(--font-moment-long);
  /* Mobile-only: roman serif a touch larger than --fs-moment-roman (0.82rem)
     for readability on phones (Petřin pokyn 2026-06-08). Desktop unchanged. */
  font-size: 0.9rem;
  line-height: var(--lh-moment-roman);
}
.fm-moment-detail__note--hand {
  font-family: var(--font-moment);
  font-size: var(--fs-moment-hand);
  font-weight: var(--fw-moment-hand);
  line-height: var(--lh-moment-hand);
}
.fm-moment-detail__note .text-expandable__body,
.fm-moment-detail__note .text-expandable__body p {
  font-family: inherit;
  font-size: inherit;
  line-height: inherit;
}
.fm-moment-detail__note .text-expandable__body p { margin: 0 0 0.5em; }
.fm-moment-detail__note .text-expandable__body p:last-child { margin-bottom: 0; }
/* More/Less = the SAME soft teal as the gathering root/echo toggle (--label-color),
   not the global violet (Petřin pokyn 2026-06-01 "more/less stejná barva jako
   v gatheringu"). */
.fm-moment-detail__note .text-expandable__toggle {
  color: var(--label-color);
  text-decoration-color: color-mix(in srgb, var(--label-color) 50%, transparent);
}
.fm-moment-detail__note .text-expandable__toggle:hover,
.fm-moment-detail__note .text-expandable__toggle:focus-visible {
  color: var(--label-color);
  text-decoration-color: currentColor;
}
/* Date BELOW the note, right-aligned — same placement + token as the gathering
   root echo date (.fm-gathering-root__date: Manrope 600/12px, --text-muted),
   Petřin pokyn 2026-06-01. */
.fm-moment-detail__dateline {
  display: flex;
  /* M13.X-ECHO-LIKES — like control LEFT, date RIGHT (mirrors the gathering root
     echo meta). Was flex-end (date only) before the heart joined this row.
     M19.X-TRANSLATE (Petřin pokyn 2026-06-12) — flex-start + date margin-left:auto
     so the 文A glyph clusters next to the heart instead of floating mid-row
     (space-between spread the three children). */
  justify-content: flex-start;
  align-items: center;
  gap: 8px;
  margin-top: 8px;
}
.fm-moment-detail__dateline .fm-moment-detail__date {
  margin-left: auto;
}
.fm-moment-detail__date {
  font: 600 12px var(--font-body);
  letter-spacing: 0.03em;
  color: var(--text-muted);
}

/* Touch-target floor for sheet interactive bits (pointer:coarse). */
@media (pointer: coarse) {
  .fm-moment-row,
  .fm-attendance,
  .fm-followups .popup__link,
  .fm-followups .popup__link-main { min-height: 44px; }
/* NOTE: .fm-gathering-header__author was REMOVED from the 44px tap floor above
   (Petřin bug 2026-06-01 — "řádky v hlavičce jsou roztažené, i v emulátoru").
   The @nick is an inline-flex TEXT link; a 44px min-height vertically centred its
   ~18px text inside a 44px box → ~13px of dead space above AND below it, which read
   as huge gaps between the gathering name / nick / location. Confirmed by rendering
   the REAL DOM with real Manrope: author box height was 44px, dropped to 18px once
   removed, gaps fell to 3–4px. The nick stays tappable at its text height; the whole
   row/profile affordance does not need the nick itself to be a 44px target. */
}
/* The follow-up pills use the desktop popup's compact `.popup__link` markup
 * (7–8px padding); on touch we lift them to the 44px tap floor + center the
 * content vertically so they read as comfortable mobile rows. */
@media (pointer: coarse) {
  .fm-followups .popup__link,
  .fm-followups .popup__link-main { align-items: center; }
}

/* ── 16. .fm-form-fullscreen — fullscreen form chrome ────────
 * Add Moment / Add Concert / Propose Landmark. Header: back · title ·
 * spacer. Body: scrolling fields. Footer: sticky full-width Save. */
.fm-form-fullscreen {
  width: 100%;
  height: 100dvh;
  display: flex;
  flex-direction: column;
  background: linear-gradient(180deg, var(--cream-surface) 0%, var(--cream-base) 100%);
  color: var(--text-dark);
  overflow: hidden;
}

.fm-form-fullscreen--moment   { background: radial-gradient(ellipse 80% 40% at 50% 0%, rgba(var(--accent-mine-rgb), 0.14) 0%, transparent 60%),      linear-gradient(180deg, var(--cream-surface) 0%, var(--cream-base) 100%); }
.fm-form-fullscreen--concert  { background: radial-gradient(ellipse 80% 40% at 50% 0%, rgba(var(--accent-turquoise-rgb), 0.10) 0%, transparent 60%), linear-gradient(180deg, var(--cream-surface) 0%, var(--cream-base) 100%); }
.fm-form-fullscreen--landmark { background: radial-gradient(ellipse 80% 40% at 50% 0%, rgba(var(--accent-copper-rgb), 0.12) 0%, transparent 60%),    linear-gradient(180deg, var(--cream-surface) 0%, var(--cream-base) 100%); }

.fm-form-fullscreen__head {
  position: sticky;
  top: 0;
  z-index: 10;
  padding: calc(env(safe-area-inset-top) + 8px) 14px 12px;
  background: linear-gradient(180deg, var(--glass-strong) 75%, transparent);
  backdrop-filter: blur(14px);
  -webkit-backdrop-filter: blur(14px);
  display: flex;
  align-items: center;
  gap: 10px;
}

.fm-form-fullscreen__back {
  width: 40px;
  height: 40px;
  border-radius: 20px;
  background: rgba(238, 240, 250, 0.06);
  border: 1px solid var(--border-on-light);
  color: var(--text-dark);
  display: grid;
  place-items: center;
  cursor: pointer;
  flex-shrink: 0;
}

.fm-form-fullscreen__title {
  flex: 1;
  text-align: center;
  font: italic 600 18px var(--font-display);
  color: var(--text-dark);
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.fm-form-fullscreen__body {
  flex: 1;
  overflow-y: auto;
  padding: 0 14px;
}

.fm-form-fullscreen__foot {
  position: sticky;
  bottom: 0;
  z-index: 10;
  padding: 12px 14px calc(env(safe-area-inset-bottom) + 18px);
  background: linear-gradient(180deg, transparent 0%, var(--glass-strong) 30%);
  backdrop-filter: blur(14px);
  -webkit-backdrop-filter: blur(14px);
}

.fm-form-fullscreen__sub {
  font: 500 11px var(--font-body);
  letter-spacing: 0.04em;
  color: var(--text-light-muted);
  text-align: center;
  margin-bottom: 8px;
}

.fm-form-fullscreen__save {
  width: 100%;
  padding: 15px;
  border-radius: 14px;
  border: 0;
  color: var(--surface-ink);
  font: 700 14px var(--font-body);
  letter-spacing: 0.04em;
  cursor: pointer;
  /* gradient + glow set by variant below */
}

.fm-form-fullscreen--moment   .fm-form-fullscreen__save { background: linear-gradient(180deg, var(--accent-mine), var(--accent-mine-deep));           box-shadow: 0 0 20px rgba(var(--accent-mine-rgb), 0.33),      0 6px 18px rgba(0, 0, 0, 0.40); }
.fm-form-fullscreen--concert  .fm-form-fullscreen__save { background: linear-gradient(180deg, var(--accent-turquoise), var(--accent-turquoise-deep)); box-shadow: 0 0 20px rgba(var(--accent-turquoise-rgb), 0.33), 0 6px 18px rgba(0, 0, 0, 0.40); }
.fm-form-fullscreen--landmark .fm-form-fullscreen__save { background: linear-gradient(180deg, var(--accent-copper), var(--accent-copper-deep));       box-shadow: 0 0 20px rgba(var(--accent-copper-rgb), 0.33),    0 6px 18px rgba(0, 0, 0, 0.40); }

/* ── 17. .fm-quick-menu — long-press / FAB action menu ───────
 * Floats over the map. Three rows max (moment / landmark / cancel). */
.fm-quick-menu {
  position: absolute;
  z-index: var(--m13-z-quick-menu);
  width: 230px;
  background: var(--glass-strong);
  backdrop-filter: blur(18px);
  -webkit-backdrop-filter: blur(18px);
  border: 1px solid var(--border-strong-light);
  border-radius: 14px;
  box-shadow: 0 24px 60px rgba(0, 0, 0, 0.60);
  color: var(--text-dark);
  overflow: hidden;
}

.fm-quick-menu__head {
  padding: 10px 14px 4px;
  font: 500 10px var(--font-body);
  letter-spacing: 0.22em;
  text-transform: uppercase;
  color: var(--text-light-muted);
  border-bottom: 1px solid rgba(238, 240, 250, 0.08);
}

.fm-quick-menu__item {
  width: 100%;
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 10px 14px;
  background: transparent;
  border: 0;
  cursor: pointer;
  color: var(--text-dark);
  text-align: left;
  border-bottom: 1px solid rgba(238, 240, 250, 0.05);
}

.fm-quick-menu__icon {
  width: 28px;
  height: 28px;
  border-radius: 8px;
  display: grid;
  place-items: center;
  flex-shrink: 0;
  /* tone-coded bg + border + icon colour per item below */
}

.fm-quick-menu__item--moment   .fm-quick-menu__icon { background: rgba(var(--accent-mine-rgb), 0.13);   border: 1px solid rgba(var(--accent-mine-rgb), 0.33);   color: var(--accent-mine); }
.fm-quick-menu__item--landmark .fm-quick-menu__icon { background: rgba(var(--accent-copper-rgb), 0.13); border: 1px solid rgba(var(--accent-copper-rgb), 0.33); color: var(--accent-copper); }
.fm-quick-menu__item--cancel   .fm-quick-menu__icon { background: color-mix(in srgb, var(--text-light-muted) 13%, transparent); border: 1px solid color-mix(in srgb, var(--text-light-muted) 33%, transparent); color: var(--text-light-muted); }

/* ── 18. .fm-fan-popup — inline preview for single fan moment ──
 * Small bubble above a single fan pin. Tap "Open" → gathering
 * sheet. The ONLY popup-style surface we keep on mobile. */
.fm-fan-popup {
  position: absolute;
  width: 230px;
  transform: translate(-50%, calc(-100% - 6px));
  background: var(--glass-strong);
  backdrop-filter: blur(16px);
  -webkit-backdrop-filter: blur(16px);
  border: 1px solid var(--border-on-light);
  border-radius: 14px;
  box-shadow: 0 18px 40px rgba(0, 0, 0, 0.55);
  color: var(--text-dark);
  padding: 12px 12px 10px;
  z-index: 1035;  /* above the Leaflet map (1000); see --m13-z scale note */
}

.fm-fan-popup__tail {
  position: absolute;
  left: 50%;
  bottom: -6px;
  width: 10px;
  height: 10px;
  transform: translateX(-50%) rotate(45deg);
  background: var(--glass-strong);
  border-right: 1px solid var(--border-on-light);
  border-bottom: 1px solid var(--border-on-light);
}

/* =============================================================
 * MO-2 — MAPPING THE REAL CHROME ONTO THE NOIR MOCKUP LOOK
 *
 * MO-1 shipped the .fm-* component CSS above as the visual target.
 * The production app, though, has its OWN chrome DOM (#map-search,
 * #test-mode-toggle, #mobile-pill-bar/.hero-tab, #events-panel,
 * #add-pin-btn, #menu-toggle, #map-legend-toggle,
 * .app-menu). This section RESKINS those existing elements at phone
 * width so the live app looks like the mockups — without forking the
 * behaviour wiring and without touching desktop (all rules are inside
 * the ≤480px guard; the legacy 481–767px mobile styles in style.css
 * are left as-is for that band).
 * ============================================================= */

/* ── MAP VIEWPORT — whole world above the tab bar (Petřin pokyn 2026-05-27) ──
 * Desktop #map is position:absolute; inset:0 (fills the whole viewport), so on
 * phones the world's bottom sat hidden behind the tab bar — the map felt
 * cramped. End the map element ABOVE the tab bar instead. _computeFillMinZoom
 * (map.js) sizes the world to the map element's height, so the entire world is
 * then visible vertically with its bottom edge resting just above the collapsed
 * tabs; the header floats over the top (map continues under it). The strip the
 * map no longer covers is exactly the opaque tab bar — no background shows.
 * "Whole world never fully visible" (ADR-046) was about WIDTH (worldCopyJump),
 * not height, so this doesn't conflict. Desktop (inset:0) untouched. */
#map {
  bottom: calc(var(--m13-tab-bar-h) + env(safe-area-inset-bottom));
}

/* ── A. TOP HEADER — 3-slot glass pill row ───────────────────
 * Slots: search lupa (left, #map-search collapsed) · gradient
 * wordmark (#fm-mobile-wordmark) · hamburger (#menu-toggle, right).
 * The legacy cluster's Add button + auth header are pulled out of
 * the header (Add → FAB; auth → behind the hamburger menu). */

/* M12D-3 S1.4 — the desktop/legacy .hero-strip (big centred Cormorant
 * title + tagline + BETA panel, z-index 1000) was never hidden on the
 * ≤480 redesign, so it rendered ON TOP of the glass wordmark pill + TEST
 * banner: two "Frisson Map" titles, two BETA chips, the frost panel
 * crowding the header. The mobile chrome replaces it entirely (wordmark
 * pill = brand, bottom tab bar = navigation), so hide it here. This also
 * removes the element that fought the search-mode takeover. */
#hero-strip.hero-strip { display: none; }

/* ── A.0. BRIDGE BAR — single glass pill spanning the header row ──
 * map-actions-handoff/mockups/01-chosen-combo.png shows the header as
 * ONE glass bar (lupa | wordmark + BETA | hamburger), not three free-
 * standing chips. Cheapest way to produce the look without touching
 * markup / search-mode JS / hero-strip / auth-header relations: paint
 * one glass capsule UNDER the existing three chips, then strip the
 * individual chip backgrounds so they read as slots inside the bar.
 * Hidden in search-expanded state (search pill takes the whole row). */
body::before {
  content: "";
  position: fixed;
  left: 14px;
  right: 14px;
  top: calc(env(safe-area-inset-top) + 6px);
  height: var(--m13-header-pill-h);
  border-radius: var(--m13-header-pill-r);
  /* Same dark glass recipe as the rail capsule (mockup shows ONE dark
   * family across both chrome elements; the global --glass-strong/soft
   * tokens read grey-blue here). Petra 2026-05-28: header pill looked
   * grey next to the rail. */
  background: linear-gradient(180deg,
    rgba(0, 0, 0, 0.90),
    rgba(4, 6, 16, 0.86));
  -webkit-backdrop-filter: blur(18px) saturate(140%);
  backdrop-filter: blur(18px) saturate(140%);
  border: 1px solid var(--border-on-light);
  box-shadow: 0 8px 24px rgba(0, 0, 0, 0.45),
              inset 0 1px 0 rgba(238, 240, 250, 0.06);
  z-index: calc(var(--m13-z-chrome) - 1);
  pointer-events: none;
  transition: opacity 200ms linear;
}
/* Fade the bridge bar out in search-mode — the teal-glow input pill
 * already covers the row. */
body:has(#map-search.map-search[data-state="expanded"])::before {
  opacity: 0;
}

/* Centre wordmark pill — only visible on phone; sits between the two
 * round glass buttons. Reuses the .fm-mobile-header__wordmark
 * type styles from MO-1 above. With the bridge bar above, the wordmark
 * is just a transparent slot inside the bar — no own glass / border /
 * shadow (would double up). */
.fm-mobile-wordmark {
  position: absolute;
  top: calc(env(safe-area-inset-top) + 6px);
  left: 50%;
  transform: translateX(-50%);
  height: var(--m13-header-pill-h);
  max-width: calc(100vw - 140px);
  padding: 0 16px;
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 8px;
  background: transparent;
  border: 0;
  box-shadow: none;
  -webkit-backdrop-filter: none;
  backdrop-filter: none;
  z-index: var(--m13-z-chrome);
  pointer-events: none; /* purely decorative */
}

/* Search bar → left header slot. Collapsed = round glass lupa; expanded
 * = teal-glow input pill that spans toward the centre (search-mode). */
#map-search.map-search {
  top: calc(env(safe-area-inset-top) + 6px);
  left: 14px;
  width: auto;
  max-width: none;
  z-index: var(--m13-z-chrome);
}

/* Collapsed state lupa = 48px touch target inside the bridge bar (no
 * own glass — bridge bar carries the surface). The toggle glyph is the
 * existing 🔍 (overridden to a clean SVG mask below). */
#map-search.map-search[data-state="collapsed"] .map-search__toggle {
  display: inline-flex;
  width: var(--m13-header-pill-h);
  height: var(--m13-header-pill-h);
  border-radius: 50%;
  background: transparent;
  border: 0;
  color: var(--text-dark);
  font-size: 1.1rem;
  box-shadow: none;
  -webkit-backdrop-filter: none;
  backdrop-filter: none;
}

/* Expanded (search mode) — the input pill morphs in place: it grows from
 * the left edge across the header with the teal focus glow. The wordmark
 * + hamburger fade away so the search owns the bar (no layout shift —
 * the search is absolutely positioned over them). */
#map-search.map-search[data-state="expanded"] {
  left: 14px;
  right: 14px;
  width: auto;
  display: flex;
  flex-direction: row;     /* base .map-search is column — force row here */
  align-items: center;
  gap: 8px;
}
#map-search.map-search[data-state="expanded"] .map-search__input-wrap {
  display: flex;
  position: relative;
  flex: 1 1 auto;          /* grows; the close × sits beside it */
  min-width: 0;
}
/* M12D-3 S1.4 — inner lupa glyph inside the expanded pill (mockup 02 +
 * m13-search-mode.md markup: the search-state pill carries a lupa on the
 * left + the input). Teal mask glyph, non-interactive. */
#map-search.map-search[data-state="expanded"] .map-search__input-wrap::before {
  content: "";
  position: absolute;
  left: 16px;
  top: 50%;
  transform: translateY(-50%);
  width: var(--space-5);        /* 20px */
  height: var(--space-5);
  background-color: var(--accent-turquoise);
  -webkit-mask: var(--fm-lupa-in) center / contain no-repeat;
  mask: var(--fm-lupa-in) center / contain no-repeat;
  pointer-events: none;
  z-index: 1;
  --fm-lupa-in: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='1.8' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='10.5' cy='10.5' r='6.5'/%3E%3Cpath d='m20 20-4.7-4.7'/%3E%3C/svg%3E");
}
#map-search.map-search[data-state="expanded"] .map-search__input {
  height: var(--m13-header-pill-h);
  padding: 0 44px 0 44px;
  border-radius: var(--m13-header-pill-r);
  background: linear-gradient(180deg, var(--glass-strong), var(--glass-soft));
  -webkit-backdrop-filter: blur(18px) saturate(180%);
  backdrop-filter: blur(18px) saturate(180%);
  border: 1px solid rgba(var(--accent-turquoise-rgb), 0.33);
  color: var(--text-dark);
  caret-color: var(--accent-turquoise);
  box-shadow: 0 8px 24px rgba(0, 0, 0, 0.45),
              0 0 0 3px rgba(var(--accent-turquoise-rgb), 0.13),
              inset 0 1px 0 rgba(238, 240, 250, 0.06);
}
/* On phone the single × beside the field is the exit-search button
 * (#map-search-close). Hide the inner clear-× so we don't show two crosses
 * stacked at the field's right edge — backspace clears while typing. */
#map-search.map-search[data-state="expanded"] .map-search__clear {
  display: none;
}

/* Exit-search × — beside the field, only in the expanded mobile state.
 * Round glass button matching the lupa/hamburger; tap collapses the search
 * (mapSearch.js) and the wordmark + hamburger fade back in. */
#map-search.map-search[data-state="expanded"] .map-search__close {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  flex: 0 0 auto;
  width: var(--m13-header-pill-h);
  height: var(--m13-header-pill-h);
  padding: 0;
  border-radius: 50%;
  background: linear-gradient(180deg, var(--glass-strong), var(--glass-soft));
  -webkit-backdrop-filter: blur(18px) saturate(180%);
  backdrop-filter: blur(18px) saturate(180%);
  border: 1px solid var(--border-on-light);
  color: var(--text-dark);
  font-size: 1.35rem;
  line-height: 1;
  cursor: pointer;
  box-shadow: 0 6px 20px rgba(0, 0, 0, 0.45),
              inset 0 1px 0 rgba(238, 240, 250, 0.06);
}
#map-search.map-search[data-state="expanded"] .map-search__close:active {
  transform: translateY(1px);
}
/* Hidden in the collapsed state (only the lupa shows there). */
#map-search.map-search[data-state="collapsed"] .map-search__close { display: none; }

/* When search is expanded, dim the decorative wordmark + hamburger so the
 * bar reads as a single search field (search-mode takeover). */
#map-search.map-search[data-state="expanded"] ~ .fm-mobile-wordmark,
body:has(#map-search.map-search[data-state="expanded"]) .fm-mobile-wordmark,
body:has(#map-search.map-search[data-state="expanded"]) .top-right-cluster .menu-toggle {
  opacity: 0;
  pointer-events: none;
}

/* LOCATE slot → lifted OUT of the capsule into the header beside the lupa on
 * phones (Petřin pokyn 2026-06-10). actionRail.js reparents the slot to <body>
 * at ≤767px — the capsule's backdrop-filter would otherwise trap a fixed child
 * to the capsule box, not the viewport. Bare teal glyph, no pebble glass (the
 * bridge bar carries the surface), matched to the lupa. */
.map-actions__slot--locate {
  position: fixed;
  top: calc(env(safe-area-inset-top) + 6px);
  /* Tuck closer to the lupa (Petřin pokyn 2026-06-10) — the lupa glyph ends
   * well before its 48px button edge, so locate slides left into that empty
   * padding instead of leaving a wide gap between the two glyphs. */
  left: calc(14px + var(--m13-header-pill-h) - 6px);
  z-index: var(--m13-z-chrome);
}
.map-actions__slot--locate .map-actions__label { display: none; }
.map-actions__slot--locate .map-actions__btn--locate {
  width: 32px;                      /* snug box — no wide centred padding */
  height: var(--m13-header-pill-h);
  background: transparent;
  border: 0;
  box-shadow: none;
  -webkit-backdrop-filter: none;
  backdrop-filter: none;
  color: var(--accent-turquoise);
  display: grid;
  place-items: center;            /* glyph centred = same baseline as the lupa */
}
.map-actions__slot--locate .map-actions__btn--locate svg {
  /* Match the search lupa exactly (Petřin pokyn 2026-06-10 — locate must not
   * outweigh the lupa): same 20px size, same teal, and NO drop-shadow (the
   * lupa mask carries none — the shadow was making locate pop). */
  width: var(--space-5);   /* 20px */
  height: var(--space-5);
}
/* Fade out in search-mode together with the wordmark + hamburger. */
body:has(#map-search.map-search[data-state="expanded"]) .map-actions__slot--locate {
  opacity: 0;
  pointer-events: none;
}
/* Search results dropdown — absolutely anchored below the pill so it does
 * NOT become a third item in the expanded flex row. Dark glass. */
#map-search .map-search__results {
  margin-top: 8px;
}
#map-search.map-search[data-state="expanded"] .map-search__results {
  position: absolute;
  top: calc(var(--m13-header-pill-h) + 8px);
  left: 0;
  right: 0;
  margin-top: 0;
}

/* Hamburger → right header slot, glass round 48px. */
.top-right-cluster {
  position: absolute;
  top: calc(env(safe-area-inset-top) + 6px);
  right: 14px;
  left: auto;
  flex-direction: row;
  align-items: center;
  gap: 0;
  z-index: var(--m13-z-chrome);
}
.top-right-cluster .menu-toggle {
  width: var(--m13-header-pill-h);
  height: var(--m13-header-pill-h);
  border-radius: 50%;
  /* No own glass — bridge bar (body::before) carries the header
   * surface; hamburger is just a transparent slot inside the bar. */
  background: transparent;
  border: 0;
  color: var(--text-dark);
  font-size: 1.15rem;
  box-shadow: none;
  -webkit-backdrop-filter: none;
  backdrop-filter: none;
}
/* Auth header collapses behind the hamburger menu on phone (per the
 * existing M-UX-AUDIT-002 pattern). */
.top-right-cluster .auth-header {
  display: none;
}

/* Backend health dot — sits below the bridge-bar (bridge bar covers from
 * safe-area-top + 6px down to + 54px). Petra 2026-05-28 phone smoke: at
 * +46 the dot was hidden under the hamburger inside the bar. Push it
 * below the bar so it floats as a free pip in the map area. */
.status--floating {
  top: calc(env(safe-area-inset-top) + 60px);
  right: 18px;
}

/* ── B. TEST MODE banner — solid gold pill below header ───────
 * The existing #test-mode-toggle IS the banner (user taps to toggle).
 * Reskin to the heavy solid-gold pill from the mockup; ink-on-gold is
 * AA-legible. Position centred below the header. */
.test-mode-toggle {
  /* JS (testMode.js _computeTestModeOffset) sets --m-test-mode-top to the
     pills' real bottom edge so TEST MODE sits tight under whatever is shown;
     the fallback (no JS / no pills) is right under the header bar. */
  top: var(--m-test-mode-top, calc(env(safe-area-inset-top) + 64px));
  left: 50%;
  right: auto;
  transform: translateX(-50%);
  z-index: var(--m13-z-chrome);
  gap: 8px;
  padding: 6px 14px 6px 8px;
  border-radius: 999px;
  background: linear-gradient(180deg, var(--accent-copper-bright), var(--accent-copper));
  border: 1px solid var(--accent-copper);
  color: var(--surface-ink);
  font-size: 0.7rem;
  letter-spacing: 0.18em;
  box-shadow: 0 6px 20px rgba(var(--accent-copper-rgb), 0.40),
              inset 0 1px 0 rgba(255, 255, 255, 0.18);
}
.test-mode-toggle:active { transform: translateX(-50%) scale(0.97); }
/* Check box → dark-on-gold tick (mockup look) regardless of on/off tint. */
.test-mode-toggle .test-mode-toggle__check {
  width: 16px;
  height: 16px;
  border-radius: 4px;
  background: var(--surface-ink);
  border-color: var(--surface-ink);
}
.test-mode-toggle .test-mode-toggle__label {
  color: var(--surface-ink);
}

/* ── B2. Hero satellite badges — day-of pills on phone ──────────
 * The desktop hero is hidden on phone, so the #hero-badges satellite row
 * re-anchors as a centred row right under the bridge bar (designer round
 * 2026-06-06, mobile variant 1 per Petra). Pill look comes from style.css
 * (noir glass + gold/mint dot) — only the placement changes here. */
.hero-badges {
  top: calc(env(safe-area-inset-top) + 64px);
  left: 0;
  right: 0;
  transform: none;
  display: flex;
  justify-content: center;
  /* ADR-295 — 4 pills on drop day must wrap into centred rows instead of
     overflowing both screen edges (style.css max-width doesn't apply here —
     left/right:0 pins the row; the padding keeps edge pills off the bezel). */
  flex-wrap: wrap;
  max-width: none;
  padding: 0 12px;
  z-index: var(--m13-z-chrome);
}
/* Badges own the row under the bar; TEST MODE drops below while any day-of
 * badge is visible (Petřin pokyn 2026-06-06: badges first, TEST MODE under
 * them). ADR-295: direct-child selector, not `button` — the map-news pill
 * became a DIV wrapping two buttons, and its inner buttons would otherwise
 * match even while the pill itself is hidden. 104px → 138px: the pills row
 * can now WRAP to two rows on drop day (ends ~131px); TEST MODE parks
 * under the worst case. Centred, so it never meets the left rail (140px). */
body:has(.hero-badges > :not([hidden])) .test-mode-toggle {
  /* No-JS fallback only: JS refines --m-test-mode-top to the pills' actual
     bottom (one row sits ~44px higher than this). 138px clears the two-row
     drop-day worst case so TEST MODE never overlaps the pills if JS lags. */
  top: var(--m-test-mode-top, calc(env(safe-area-inset-top) + 138px));
}

/* ── C. SIDE BUTTONS — floating map buttons (left/right stacks) ──
 * map-actions-handoff fix (m14-glass-rail.md + mockups/01-chosen-combo.png):
 * the three former round side buttons (Landmark gold / note iris / legend
 * mist) were indistinguishable from the round CLUSTER bubbles. They become
 * rounded-SQUARE tiles, tightly stacked, floating over ONE glass capsule
 * (.fm-action-rail-frame) so the trio reads as a single "toolbox" of
 * CONTROLS. Each button keeps its own DOM + JS wiring — only the skin +
 * position change. The right side of the map is now empty. Legend joins the
 * rail as the 3rd tile (Petřin pokyn 2026-05-27). */

/* THE glass capsule = #map-actions itself (M13.X-PEBBLES, ADR-247). A real
 * flex column: the visible slots are its children, so the capsule height
 * always hugs the content — no hole at 3, 4 or 5 tiles (the old decorative
 * .fm-action-rail-frame had a hardcoded 5-tile height + per-tile fixed
 * `top:` steps; removing Songs per ADR-237 left a gap). Slot order comes
 * from actionRail.js RAIL_ORDER (single source of truth). Dark glass skin
 * carried over 1:1 (Petra 2026-05-28: "černé pozadí ... jako v mockupu");
 * width is content-driven so the new micro-captions (ADR-247 R3) fit. */
#map-actions.map-actions {
  position: fixed;
  left: var(--m14-rail-left);
  right: auto;
  /* ADR-295 (Petřin pokyn 2026-06-10, iter 2) — anchored below the
   * satellite pills row incl. its TEST MODE second row, so the pills can
   * NEVER overlap the tiles (full centering was "moc" — too low). */
  top: calc(env(safe-area-inset-top) + var(--m14-rail-top));
  transform: none;                 /* desktop centers via translateX(-50%) */
  width: auto;
  min-width: var(--m14-rail-w);
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--m14-rail-gap);
  padding: var(--m14-rail-padding);
  border-radius: var(--m14-rail-radius);
  background: linear-gradient(180deg,
    rgba(0, 0, 0, 0.90),
    rgba(4, 6, 16, 0.86));
  -webkit-backdrop-filter: blur(18px) saturate(140%);
  backdrop-filter: blur(18px) saturate(140%);
  border: 1px solid var(--border-on-light);
  box-shadow: 0 8px 24px rgba(0, 0, 0, 0.45),
              inset 0 1px 0 rgba(238, 240, 250, 0.06);
  z-index: var(--m13-z-chrome);
}

/* Slot = tile + its micro-caption, stacked. */
#map-actions .map-actions__slot {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--m14-rail-caption-gap);
}

/* Micro-captions under the tiles (ADR-247 R3 — "opravdu malé světlé písmo
 * uvnitř kapsule, bez pill chrome"). Strips the desktop glass-pill look. */
#map-actions .map-actions__label {
  /* Captions dropped on mobile (Petřin pokyn 2026-06-10) — icon-only rail is
   * more compact; the buttons keep their aria-label/title for a11y + desktop
   * tooltips. Flip back to `block` to restore the JOIN/PLACES/HUB micro-caps. */
  display: none;
  min-width: 0;
  padding: 0;
  background: none;
  border: none;
  box-shadow: none;
  font-size: 9px;                  /* M-rail-shrink 2026-06-10 (was --fs-3xs 10px) — keeps "PLACES" from outgrowing the 34px tile */
  font-weight: 600;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: rgba(238, 240, 250, 0.72);
  text-shadow: 0 1px 2px rgba(0, 0, 0, 0.65);
}

/* Tile — Places (gold). The spider container is a normal flex child now;
 * `relative` keeps it the anchor for its absolutely-positioned submenu,
 * which still pops downward into the map. */
.spider-menu {
  position: relative;
  top: auto;
  left: auto;
  right: auto;
  bottom: auto;
}
/* While the submenu is open, lift the whole CAPSULE above the surrounding
 * chrome. The submenu's z-index is trapped inside #map-actions' stacking
 * context (fixed + z-chrome), so without this the later-painted floating
 * chrome could draw over the open menu — same trap as the 2026-06-01
 * "legend pebble punches through the menu" bug, one level up
 * (reference_popover_trapped_in_container_stacking_context). Open-only so
 * the rail drops back below bottom sheets when the menu closes. */
#map-actions:has(.spider-menu--open) {
  z-index: 1090;
}
.spider-menu .spider-menu__button {
  width: var(--m14-rail-tile);
  height: var(--m14-rail-tile);
  box-sizing: border-box;          /* keep total render = 40px so the step matches the gap */
  border-radius: var(--m14-rail-tile-radius);
  /* Tone-tinted window in the dark capsule — wash + ring strong enough to
   * read at a glance (Petra 2026-05-28 iter 5: "trochu výraznější, rámečky
   * příliš tenké"); still no solid colour fill so tiles don't compete with
   * the round cluster bubbles. Glyph stays tone-bright. */
  background:
    radial-gradient(circle at 35% 30%,
      rgba(var(--accent-copper-rgb), 0.30),
      rgba(var(--accent-copper-rgb), 0.08) 85%);
  border: 1.5px solid rgba(var(--accent-copper-rgb), 0.70);
  color: var(--accent-copper-bright);
  /* Inset tone glow removed — was bleeding into the glyph (Petra
   * 2026-05-28 iter 7: "ikony splývají se září"). Keep only the top
   * highlight, let the glyph float over the wash. */
  box-shadow: inset 0 1px 0 rgba(238, 240, 250, 0.10);
}
/* M15 ⑧ LANDMARK-HIGHLIGHT (ADR-266) — Places tile active = same recipe as
 * the mobile JOIN active (thicker ring + bold glow, resting look stays),
 * copper family colour. Needs 0,3,0 specificity: the square-tile skin above
 * (0,2,0) beats the desktop 0,1,0 rule in style.css, so the ring never
 * showed on phones (Petřin screenshot 2026-06-06). */
.spider-menu .spider-menu__button.spider-menu__button--active {
  border-color: var(--accent-copper);
  box-shadow:
    0 0 0 2.5px var(--accent-copper),
    0 0 18px rgba(var(--accent-copper-rgb), 0.85),
    0 0 34px rgba(var(--accent-copper-rgb), 0.40),
    inset 0 1px 0 rgba(238, 240, 250, 0.10);
}

.spider-menu .spider-menu__icon {
  color: var(--accent-copper-bright);
  /* Drop-shadow halo for sharp glyph readability against the wash. */
  filter: drop-shadow(0 1px 2px rgba(0, 0, 0, 0.80));
}
.spider-menu .spider-menu__icon use,
.spider-menu .spider-menu__icon path { stroke-width: 2.4; }

/* (Songs tile removed — M13.X-SONGS-TAB ADR-237: the song world moved to a
 * first-class panel tab; rail tiles below shifted up one step.) */

/* Legend → standalone round pebble bottom-left (Petřin pokyn 2026-06-10) —
 * no longer reparented into the rail. Mirrors the "+ ECHO" FAB on the
 * opposite corner: same bottom offset above the tab bar, dark-glass disc
 * skin from the bridge bar / capsule family so it reads as the same chrome.
 * Icon-only (the LEGEND caption is hidden below). */
.map-legend-toggle {
  position: fixed;
  left: 14px;
  /* Vertically centre the disc on the "+ ECHO" FAB (Petřin pokyn 2026-06-10).
   * The FAB sits at the +16px base offset and is var(--m14-fab-height) (52px)
   * tall; this disc is 40px. To align their CENTRES (not their bottoms) lift
   * the disc by half the height difference — derived from the token so it
   * stays centred if the FAB height ever changes. */
  bottom: calc(var(--m13-tab-bar-h) + env(safe-area-inset-bottom) + 16px + (var(--m14-fab-height) - 40px) / 2);
  /* Disc size (Petřin pokyn 2026-06-10): 44px was too dominant, 36px left too
   * little ring — 40px keeps the 26px glyph with a calm, even halo. */
  width: 40px;
  height: 40px;
  min-height: 40px;                  /* override desktop min-height:44px */
  box-sizing: border-box;
  padding: 0;
  border-radius: 50%;
  background: linear-gradient(180deg,
    rgba(0, 0, 0, 0.90),
    rgba(4, 6, 16, 0.86));
  -webkit-backdrop-filter: blur(18px) saturate(140%);
  backdrop-filter: blur(18px) saturate(140%);
  border: 1px solid var(--border-on-light);
  color: var(--text-light);
  display: grid;
  place-items: center;
  box-shadow: 0 8px 24px rgba(0, 0, 0, 0.45),
              inset 0 1px 0 rgba(238, 240, 250, 0.06);
  z-index: var(--m13-z-chrome);
}
.map-legend-toggle .map-legend-toggle__icon {
  color: var(--text-light);
  filter: drop-shadow(0 1px 2px rgba(0, 0, 0, 0.80));
  /* Thin stroke (Petřin pokyn 2026-06-10 "netučni ho") — at 26px the old 2.4
   * tile-parity weight read bold and the i-stem fused with the dot. */
  stroke-width: 1.4;
}
.map-legend-toggle .map-legend-toggle__label { display: none; }
.map-legend-toggle .map-legend-toggle__icon { color: var(--text-muted); }

/* Pressed feedback — depress 1px (mockup: ~120ms ease-out). */
.spider-menu .spider-menu__button:active,
.map-legend-toggle:active {
  transform: translateY(1px);
}

/* Legend on phone opens in the ONE bottom-sheet engine (mapLegend.js →
 * bottomSheet.js), same as every other detail — the desktop popover card
 * is never shown at this width, so it needs no mobile positioning. */

/* ── C2. CLEAN SIDE-BUTTON GLYPHS (M12D-3 S1.1 / S1.5 / S1.6) ──
 * The legacy chrome uses emoji glyphs (🏛️ museum, 🎵 note, 🔍 lupa).
 * Emoji are full-colour bitmap glyphs: they ignore the tone `color`,
 * render bumpy/inconsistent across platforms, and don't match the
 * mockup's clean monochrome line icons. At phone width we hide the
 * emoji and paint a crisp mask-based SVG that inherits currentColor
 * (= the button's tone token). Desktop is untouched — these rules are
 * inside the ≤480px guard and target the emoji span only. */

/* Rail glyphs are real sprite <svg>s sized + tinted directly via currentColor
 * (the old emoji+mask painter was retired once both the landmark spider button
 * — #icon-columns — and the songs pebble — #icon-note — moved to sprites). */

/* Landmark spider button — real sprite <svg> (#icon-columns), copper tone
 * from the rule above. Size it to the rail tile glyph scale; the glyph
 * paints its own copper strokes via currentColor (no mask needed). */
.spider-menu .spider-menu__icon {
  width: var(--space-5);        /* 20px */
  height: var(--space-5);
}

/* ── C2.5. SPIDER SUBMENU — pop DOWN at phone width ───────────────
 * Petřin screenshot 2026-05-28 — submenu (Find nearest landmark / Guided
 * tour) je oříznutý nahoře. Příčina: style.css:19465 přidává @media
 * (max-width: 767px) override který submenu posunul NAD button
 * (`bottom: calc(100% + 0.6rem)`) — předpokládalo se, že spider sedí
 * ve spodním levém rohu. V M12 mobilním redesignu ale spider rail tile
 * sedí NAHOŘE v levém sloupci (`top: var(--m14-rail-tile-top)`), takže
 * "pop up" submenu spadne mimo viewport. Force back to pop DOWN at
 * phone width — under the button, into the map area, where there's room.
 * Arrow tail also flips back to "point UP at button above". */
.spider-menu .spider-menu__list {
  top: calc(100% + 0.6rem);
  bottom: auto;
}
.spider-menu .spider-menu__list::before {
  top: -8px;
  bottom: auto;
  left: 22px;
  border-top: none;
  border-bottom: 8px solid rgba(var(--accent-copper-rgb), 0.22);
}
.spider-menu .spider-menu__list::after {
  top: -7px;
  bottom: auto;
  left: 23px;
  border-top: none;
  border-bottom: 7px solid rgba(var(--cream-surface-rgb), 0.98);
}

/* (Songs pebble removed entirely — M13.X-SONGS-TAB ADR-237.) */

/* Search lupa (teal) — the collapsed-state toggle only renders on
 * mobile (display:none on desktop), so swapping its glyph here is
 * phone-only. Clean magnifying-glass mask in place of 🔍. */
#map-search.map-search[data-state="collapsed"] .map-search__toggle > span[aria-hidden] {
  font-size: 0;
  width: var(--space-5);        /* 20px */
  height: var(--space-5);
  display: inline-block;
  background-color: var(--accent-turquoise);
  -webkit-mask: var(--fm-lupa) center / contain no-repeat;
  mask: var(--fm-lupa) center / contain no-repeat;
  --fm-lupa: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='1.8' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='10.5' cy='10.5' r='6.5'/%3E%3Cpath d='m20 20-4.7-4.7'/%3E%3C/svg%3E");
}

/* Legend (ⓘ) glyph — bumped up (Petřin pokyn 2026-06-10 "v té pilulce je to
 * zbytečně malé"): the standalone bottom-left disc is 44px, so a 20px glyph
 * floated small inside it. 26px fills the disc with breathing room. */
.map-legend-toggle .map-legend-toggle__icon {
  width: 26px;
  height: 26px;
}

/* ── D. FAB — primary Add (#add-pin-btn) → capsule "+ MOMENT" ──
 * map-actions-handoff (m14-capsule-fab.md + mockups/01-chosen-combo.png):
 * the round 60px magenta + bubble became a labeled pill. Two glyphs:
 * an SVG-shaped + on the LEFT (painted as ::before), then the word
 * "MOMENT" on the RIGHT (painted as ::after). The real i18n text node
 * (#add-pin-btn = "+ Add moment") is collapsed via font-size:0 so it
 * stays in the DOM (a11y / JS hooks) but never paints on phone. This
 * keeps desktop's "+ Add moment" label untouched + sidesteps the
 * "+ Add moment" / "+ MOMENT" naming reshuffle (no i18n / markup
 * surgery for a mobile-only chrome change).
 *
 * Sizing per handoff: height 60px, width content-driven (~145px),
 * padding 0 22px 0 18px (asymmetric, icon side tighter), border-radius
 * 30px (full pill), label Manrope 800 / 13px / 0.12em tracking. */
[id="add-pin-btn"].add-pin-btn {
  position: fixed;
  right: 18px;
  bottom: calc(var(--m13-tab-bar-h) + env(safe-area-inset-bottom) + 16px);
  height: var(--m14-fab-height);   /* 44px — shrunk from 60 (Petra 2026-05-30) */
  width: auto;                     /* content-driven pill */
  min-width: 0;
  padding: var(--m14-fab-pad);
  border-radius: var(--m14-fab-radius);
  border: 1.5px solid var(--accent-mine);
  background: linear-gradient(180deg, var(--accent-mine), var(--accent-mine-deep));
  color: var(--text-light);
  font-size: 0;                    /* collapse legacy "+ Add moment" text */
  line-height: 1;
  white-space: nowrap;
  z-index: var(--m13-z-chrome);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--m14-fab-gap);
  box-shadow: 0 0 22px rgba(var(--accent-mine-rgb), 0.46),
              0 9px 22px rgba(0, 0, 0, 0.54);
}
/* The "+" icon — drawn left of the label. Stroke-only SVG via mask so
 * it inherits the button's text color (= --text-light moon). Sized to
 * match the M14 spec (24×24 in-flow). */
[id="add-pin-btn"].add-pin-btn::before {
  content: "";
  flex: 0 0 auto;
  width: 21px;
  height: 21px;
  background-color: currentColor;
  -webkit-mask: var(--fm-fab-plus) center / 21px 21px no-repeat;
  mask: var(--fm-fab-plus) center / 21px 21px no-repeat;
  --fm-fab-plus: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2.2' stroke-linecap='round'%3E%3Cpath d='M12 5v14M5 12h14'/%3E%3C/svg%3E");
}
/* The "ECHO" label. M12.X-I18N-AUDIT (Petřin pokyn 2026-05-31): the text
 * moved out of CSS `content:` into a real i18n-driven DOM span
 * (.add-pin-btn__fab-label, key addPin.fabLabel) so it's localizable like
 * everything else. The desktop label span (.add-pin-btn__text =
 * addPin.button) is collapsed here — the FAB shows the ::before "+" icon
 * + the short "Echo" word. Manrope 800 uppercase per handoff. */
[id="add-pin-btn"].add-pin-btn .add-pin-btn__text {
  display: none;
}
[id="add-pin-btn"].add-pin-btn .add-pin-btn__fab-label {
  display: inline;
  font-family: var(--font-body);
  font-weight: var(--m14-fab-label-weight);
  font-size: var(--m14-fab-label-size);
  letter-spacing: var(--m14-fab-label-track);
  text-transform: uppercase;
  color: var(--text-light);
  line-height: 1;
}
[id="add-pin-btn"].add-pin-btn:active {
  transform: translateY(1px);
  box-shadow: 0 0 18px rgba(var(--accent-mine-rgb), 0.36),
              0 6px 14px rgba(0, 0, 0, 0.50);
}

/* FAB quick menu — anchored bottom-right above the FAB. Reuses the
 * .fm-quick-menu component CSS from MO-1; here we just position it and
 * its backdrop. */
.fm-quick-menu {
  position: fixed;
  right: 18px;
  bottom: calc(var(--m13-tab-bar-h) + env(safe-area-inset-bottom) + 16px + var(--m14-fab-height) + 10px);
  left: auto;
}
.fm-quick-menu[hidden] { display: none; }
.fm-quick-menu__label {
  font: 600 13px var(--font-body);
  color: var(--text-dark);
}
.fm-quick-menu__item--cancel .fm-quick-menu__label { color: var(--text-light-muted); }
.fm-quick-menu-backdrop {
  position: fixed;
  inset: 0;
  background: rgba(5, 7, 18, 0.35);
  z-index: var(--m13-z-quick-menu);
}
.fm-quick-menu-backdrop[hidden] { display: none; }
.fm-quick-menu { z-index: calc(var(--m13-z-quick-menu) + 1); }

/* ── E. BOTTOM TAB BAR — #mobile-pill-bar / .hero-tab ────────
 * The centred floating pill capsule becomes a full-width bottom tab bar
 * (4 equal tabs, tone-coded active, z 45, safe-area). Reuses .fm-tab
 * tone classes by mapping the real tab ids → tones below. */
.mobile-pill-bar {
  display: flex;
  position: fixed;
  left: 0;
  right: 0;
  bottom: 0;
  top: auto;
  transform: none;
  width: 100%;
  height: calc(var(--m13-tab-bar-h) + env(safe-area-inset-bottom));
  padding: 4px 10px calc(6px + env(safe-area-inset-bottom));
  border-radius: 0;
  gap: 4px;
  align-items: stretch;
  background: linear-gradient(180deg, var(--glass-strong), var(--glass-strong));
  -webkit-backdrop-filter: blur(20px) saturate(180%);
  backdrop-filter: blur(20px) saturate(180%);
  border: 0;
  border-top: 1px solid var(--border-on-light);
  z-index: var(--m13-z-tab-bar);
  box-shadow: 0 -8px 24px rgba(0, 0, 0, 0.45);
}

/* Each hero tab → .fm-tab look: stacked, uppercase, tone-coded active. */
.mobile-pill-bar .hero-tab {
  flex: 1;
  min-width: 0;
  min-height: auto;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 3px;
  padding: 4px 2px;
  border-radius: 14px;
  background: transparent;
  color: var(--text-muted);
  font: 500 10px var(--font-body);
  letter-spacing: 0.06em;
  text-transform: uppercase;
  position: relative;
  box-shadow: none;
}
.mobile-pill-bar .hero-tab.is-active { font-weight: 700; box-shadow: none; }

/* S1.3 — tab line icon above the label (mockup 01 + m13-mobile-chrome §5).
 * Inactive icons sit at the muted label colour; active tab tints both icon
 * and label to its tone via the currentColor cascade on .is-active above. */
.mobile-pill-bar .hero-tab .hero-tab__icon {
  width: var(--space-5);        /* 20px */
  height: var(--space-5);
  display: block;
  color: inherit;
  opacity: 0.72;
  transition: opacity 200ms;
}
.mobile-pill-bar .hero-tab.is-active .hero-tab__icon { opacity: 1; }
.mobile-pill-bar .hero-tab .hero-tab__label { display: block; line-height: 1; }

/* Tone-code the active tab by its data-tab (Moments=violet,
 * Concerts=teal, Online=azure, Yours=magenta). */
.mobile-pill-bar .hero-tab[data-tab="pins"].is-active {
  color: var(--accent-violet-soft);
  background: linear-gradient(180deg, rgba(var(--accent-violet-rgb), 0.13), transparent);
}
.mobile-pill-bar .hero-tab[data-tab="events"].is-active {
  color: var(--accent-turquoise);
  background: linear-gradient(180deg, rgba(var(--accent-turquoise-rgb), 0.13), transparent);
}
.mobile-pill-bar .hero-tab[data-tab="online"].is-active {
  color: var(--accent-online-bright);
  background: linear-gradient(180deg, rgba(var(--accent-online-rgb), 0.13), transparent);
}
.mobile-pill-bar .hero-tab[data-tab="yours"].is-active {
  color: var(--accent-mine);
  background: linear-gradient(180deg, rgba(var(--accent-mine-rgb), 0.13), transparent);
}
/* M13.X-SONGS-TAB (ADR-237) — Songs = gold, the rating world's colour
 * (stars / budget / chart crown). Bright variant for text-on-noir parity
 * with the violet-soft pattern above. */
.mobile-pill-bar .hero-tab[data-tab="songs"].is-active {
  color: var(--accent-copper-bright);
  background: linear-gradient(180deg, rgba(var(--accent-copper-rgb), 0.13), transparent);
}
/* Active glow strip on top edge (currentColor = the tone above). */
.mobile-pill-bar .hero-tab.is-active::after {
  content: "";
  position: absolute;
  top: 3px;
  left: 50%;
  transform: translateX(-50%);
  width: 24px;
  height: 3px;
  border-radius: 2px;
  background: currentColor;
  box-shadow: 0 0 8px currentColor;
}

/* Echoes tab counter (Petřin pokyn 2026-06-07 — „kvůli mobilu, aby to
 * lidi lákalo kliknout"): with the sheet CLOSED the count must stay
 * visible on the bottom-bar pill even though Echoes is the (default)
 * active tab — that's the tap invitation. Only while the sheet is OPEN
 * on that tab (user already looks at the content) it hides. */
body:has(#events-panel.events-panel.is-expanded)
  .mobile-pill-bar
  .hero-tab.is-active
  .fm-tab-count--muted {
  display: none;
}

/* ── F. TAB LIST PANEL — #events-panel as a DRAGGABLE LIST SHEET ──
 * Petra 2026-05-26 (Google-Maps "Uloženo" reference): the expanded tab is a
 * bottom sheet you can DRAG DOWN to dismiss — NOT a fullscreen overlay and
 * NOT closed with an ×. It opens at a medium height, drags up to a tall snap
 * that STILL leaves a strip of map at the top (a tab list never covers the
 * whole map), and drags down past medium to return to the map. Drag logic +
 * snap classes are wired in mobileChrome.js. */
#events-panel.events-panel {
  left: 0;
  right: 0;
  top: auto;                       /* bottom-anchored sheet (was top:0 fullscreen) */
  bottom: calc(var(--m13-tab-bar-h) + env(safe-area-inset-bottom));
  height: calc((100dvh - var(--m13-tab-bar-h)) * var(--m13-panel-half-ratio));  /* default = medium */
  z-index: var(--m13-z-tab-panel);
  max-height: none;
  border: 0;
  border-top-left-radius: var(--m13-sheet-radius);
  border-top-right-radius: var(--m13-sheet-radius);
  /* Square bottom corners — the sheet sits flush above the tab bar; rounding
   * here exposed light "holes" at the bottom edge (Petřin telefon 2026-05-27).
   * The base .events-panel rounds all corners, so override explicitly. */
  border-bottom-left-radius: 0;
  border-bottom-right-radius: 0;
  padding-bottom: 0;
  background: linear-gradient(180deg, var(--cream-mist) 0%, var(--cream-surface) 100%);
  -webkit-backdrop-filter: none;
  backdrop-filter: none;
  box-shadow: 0 -18px 50px rgba(0, 0, 0, 0.55);
  overflow: hidden;                /* clip rounded corners; the body scrolls inside */
  transition: var(--m13-sheet-transition, height 240ms cubic-bezier(0.22, 1, 0.36, 1));
}
#events-panel.events-panel.is-expanded { display: flex; flex-direction: column; }

/* Tall snap — grows upward but STILL leaves --m13-panel-full-top of map at
 * the top (never the whole screen). Toggled by mobileChrome.js on drag. */
#events-panel.events-panel.panel-snap-full {
  height: calc(100dvh - var(--m13-tab-bar-h) - env(safe-area-inset-bottom) - var(--m13-panel-full-top));
}

/* Chrome visibility while the list sheet is open: the map-floating chrome the
 * sheet sits over (side buttons, songs, legend, FAB, status) is hidden whenever
 * the sheet is open at any height. The top hero (search · menu · wordmark ·
 * TEST MODE) now STAYS visible even at full — full stops below the TEST MODE
 * banner, so there's no collision and TEST MODE must never be covered
 * (Petřin pokyn 2026-05-27). */
/* M13.X-PEBBLES: the whole rail is ONE element now (#map-actions), so hiding
 * it covers every tile — including JOIN + LOCATE, which the old per-tile
 * selector list missed (pre-existing ADR-214 gap: those two tiles stayed
 * floating over the open sheet). */
body:has(#events-panel.events-panel.is-expanded) #map-actions.map-actions,
body:has(#events-panel.events-panel.is-expanded) #map-legend-toggle,
body:has(#events-panel.events-panel.is-expanded) [id="add-pin-btn"].add-pin-btn,
body:has(#events-panel.events-panel.is-expanded) .status--floating {
  visibility: hidden;
  pointer-events: none;
}

/* The legacy drag-pip toggle + tab strip + topbar are replaced by the
 * dedicated fullscreen header (#fm-panel-header). Hide the old chrome. */
#events-panel .events-panel__toggle,
#events-panel .events-panel__header,
#events-panel .events-panel__topbar,
#events-panel .events-panel__close {
  display: none;
}

/* Reveal the mobile fullscreen panel header. */
#fm-panel-header.fm-tab-panel__head {
  display: flex;
}
#fm-panel-header[hidden] { display: none; }
.fm-tab-panel__head-text { flex: 1; min-width: 0; }

/* No × on a tab list (Petra 2026-05-26: "tabs don't have a × on mobile").
 * The sheet is dismissed by dragging the grip down, or by tapping the active
 * tab again. Hide the close button entirely on phone. */
.fm-tab-panel__close { display: none; }

/* Drag grip — the Google-Maps pill at the very top of the list sheet. Full-
 * width hit area, centred bar, owns the pull-to-resize / pull-to-dismiss
 * gesture (wired in mobileChrome.js). */
.fm-tab-panel__grip {
  flex: 0 0 auto;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 10px 0 6px;
  touch-action: none;
  cursor: grab;
  user-select: none;
}
/* The grip carries a `hidden` attribute (so it stays out of the desktop
 * sidebar). On mobile, while the list sheet is open, force it visible —
 * style.css has a global `[hidden] { display: none !important }`, so a plain
 * class rule can't win. An id selector + !important does (id beats the
 * attribute selector even when both are !important). THIS is why the grip
 * never showed (Petřin telefon 2026-05-27); the bar colour was a red herring. */
#events-panel.is-expanded #fm-panel-grip {
  display: flex !important;
}
.fm-tab-panel__grip:active { cursor: grabbing; }
.fm-tab-panel__grip-bar {
  width: 44px;
  height: 5px;
  border-radius: 3px;
  /* Solid rgba (NOT color-mix) — color-mix is unsupported on older mobile
   * Safari, where it collapsed to transparent and the grip vanished
   * (Petřin telefon 2026-05-27). This light value reads clearly on the
   * dark sheet. */
  background: rgba(238, 240, 250, 0.5);
  transition: background 200ms;
}
.fm-tab-panel__grip:active .fm-tab-panel__grip-bar {
  background: rgba(238, 240, 250, 0.8);
}

/* Panel body scrolls inside the fullscreen shell. */
#events-panel .events-panel__body {
  flex: 1;
  min-height: 0;
  overflow-y: auto;
  -webkit-overflow-scrolling: touch;
  /* M13.X-GATHERING-POLISH (Petřin bug 2026-06-01 — "karty v seznamu jsou moc úzké,
     plýtvá to místem") — the list cards sat behind a DOUBLE side gutter: 16px here +
     ~11px on .feed-list = ~27px each side before the card even started. Trim this
     panel inset so the cards (echoes / concerts / online) run wider. The .feed-list
     side padding below is trimmed too; together they leave ~13px each side. */
  padding: 6px 8px 14px;
}
/* Card stack side gutter — paired with the trimmed panel inset above so mobile list
   cards use the available width instead of wasting ~27px each side (Petřin bug
   2026-06-01). Vertical rhythm (0.35rem top / 0.5rem gap / 0.5rem bottom) unchanged. */
#events-panel .events-panel__body .feed-list {
  padding-left: 0.3rem;
  padding-right: 0.3rem;
}
/* Mobile-only: Echo feed roman serif a touch larger than --fs-moment-roman
   (0.82rem) for phone readability (Petřin pokyn 2026-06-08). Matches the echo
   detail note bump above; --hand (handwriting) voice unchanged. Desktop feed
   keeps the style.css size. */
.feed-list__item--pin .feed-list__pin-text:not(.feed-list__pin-text--hand) {
  font-size: 0.9rem;
}
/* Mobile-only: the echo composer textarea matches the same roman bump
   (0.82→0.9rem) when it's in its long-form (.is-roman) voice — both creating a
   new echo and editing reuse this single #comment-input (Petřin pokyn
   2026-06-08). The Caveat handwriting voice (#comment-input without .is-roman)
   keeps --fs-moment-hand. */
#comment-input.is-roman {
  font-size: 0.9rem;
}

/* ── G. BOTTOM-SHEET HAMBURGER MENU — .app-menu V5 ────────────────
 * V5 (Petřin schválení 2026-05-28 po nezávislém UX review). Bottom-
 * sheet pattern matching Concert/Event/POI detail sheets — drawer
 * slides UP from bottom edge, rounded only at top corners, grip pill
 * nahoře jako univerzální mobile signal "dismissible sheet", content-
 * fit height (žádný arbitrary 82vh container z V4 = mrtvý prostor dole).
 *
 * Hero card = tap zone → otevře Profile (identity-as-primary-CTA,
 * Spotify/Twitter pattern). Profile řádek z nav listu odpadl, hero ho
 * nahrazuje. Žádné chevrony u Security/Preferences/Data — ty otevírají
 * modal (ne drill-down screen), chevron by byl falešný affordance.
 * Backstage = card s ADMIN chipem (dark text na bright copper = vysoký
 * WCAG kontrast, ne muted-on-glow jako V4). About/Privacy/Terms VEN
 * z menu (legal linky patří do page footer, ne primárního nav menu). */
.app-menu {
  top: auto;
  left: 0;
  right: 0;
  bottom: 0;
  width: auto;
  z-index: var(--m13-z-quick-menu);
  pointer-events: none;
}
.app-menu.is-open {
  pointer-events: auto;
  animation: none;                  /* override legacy desktop appMenuPop */
}
.app-menu .app-menu__backdrop {
  position: fixed;
  inset: 0;
  background: rgba(0, 0, 0, 0.55);
  opacity: 0;
  transition: opacity 220ms cubic-bezier(0.22, 1, 0.36, 1);
  pointer-events: none;
}
.app-menu.is-open .app-menu__backdrop {
  opacity: 1;
  pointer-events: auto;
}
.app-menu .app-menu__panel {
  position: fixed;
  top: auto;
  left: 0;
  right: 0;
  bottom: 0;
  height: auto;
  max-height: min(75vh, 680px);
  padding:
    var(--space-2)
    var(--space-4)
    calc(env(safe-area-inset-bottom) + var(--space-5));
  background: var(--glass-strong);
  -webkit-backdrop-filter: blur(20px) saturate(180%);
  backdrop-filter: blur(20px) saturate(180%);
  border: 0;
  border-top: 1px solid var(--border-strong-light);
  border-radius: var(--radius-xl) var(--radius-xl) 0 0;
  box-shadow: 0 -18px 50px rgba(0, 0, 0, 0.60);
  color: var(--text-dark);
  gap: var(--space-1-5);
  display: flex;
  flex-direction: column;
  transform: translateY(100%);
  transition: transform 240ms cubic-bezier(0.22, 1, 0.36, 1);
  overflow-y: auto;
  -webkit-overflow-scrolling: touch;
}
.app-menu.is-open .app-menu__panel {
  transform: translateY(0);
}
@media (prefers-reduced-motion: reduce) {
  .app-menu .app-menu__panel,
  .app-menu .app-menu__backdrop {
    transition: none;
  }
}

/* Drag grip — bottom-sheet "dismissible" signal. Visible bar je 44×5
 * pill, ale wrapper má bigger hit area (28px tall) pro touch ergonomii
 * (Petřin pokyn 2026-05-28 "grip nefunguje"). Vlastní drag-to-dismiss
 * handler žije v mobileMenu.js — spring-back pod 64px threshold,
 * closeMenu() nad. */
.app-menu .app-menu__grip {
  display: grid;
  place-items: center;
  width: 100%;
  height: 28px;
  margin: 0 0 4px;
  flex-shrink: 0;
  cursor: grab;
  touch-action: none;           /* prevent native page-scroll capture */
}
.app-menu .app-menu__grip:active {
  cursor: grabbing;
}
.app-menu .app-menu__grip::before {
  content: "";
  display: block;
  width: 44px;
  height: 5px;
  border-radius: 999px;
  background: rgba(238, 240, 250, 0.45);
  transition: background 160ms ease;
}
.app-menu .app-menu__grip:active::before {
  background: rgba(238, 240, 250, 0.7);
}

/* × close odstraněn (V5 iter 6, Petřin pokyn 2026-05-28 "když tam je
 * grip, odstraň x"): grip drag-to-dismiss + backdrop tap +
 * ESC stačí. Markup span také odebrán z index.html. */

/* Hero card — whole row is a button (tap zone → Profile). Avatar +
 * username, žádný right reserve (× close odstraněn ve V5 iter 6). */
.app-menu .app-menu__hero {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  width: 100%;
  padding: var(--space-2);
  border: 0;
  background: transparent;
  border-radius: var(--radius-lg);
  cursor: pointer;
  text-align: left;
  font: inherit;
  color: var(--text-dark);
  transition: background 160ms ease, transform 100ms ease;
}
.app-menu .app-menu__hero:hover,
.app-menu .app-menu__hero:focus-visible {
  background: rgba(var(--accent-violet-rgb), 0.10);
  outline: none;
}
.app-menu .app-menu__hero:active {
  background: rgba(var(--accent-violet-rgb), 0.18);
  transform: scale(0.99);
}
.app-menu .app-menu__hero-avatar {
  /* V5 fix (Petřin pokyn 2026-05-28 "musí být jednotný"): container je
   * čistý — žádný background/border/text/initial placeholder. mobileMenu.js
   * sem injektuje REAL user avatar SVG přes avatarSvg() — sdílený renderer
   * napříč markery / popupy / profilem / adminem. SVG si kreslí vlastní
   * circular gradient + per-avatar --av-glow halo (memory
   * reference_avatar_rendering_unify_all_surfaces). */
  flex-shrink: 0;
  width: 54px;
  height: 54px;
  display: inline-block;
  line-height: 0;
}
.app-menu .app-menu__hero-avatar > svg {
  width: 100%;
  height: 100%;
}
.app-menu .app-menu__hero-text {
  display: flex;
  flex-direction: column;
  gap: 2px;
  min-width: 0;
  flex: 1;
}
.app-menu .app-menu__hero-name {
  /* V5 iter 3 (Petřin pokyn 2026-05-28 "nickname by měl být magenta, je
   * to napříš pro moje věci") — username = identity ⇒ user's-own tone,
   * konzistentní s --accent-mine napříč app (moje pin highlights, own
   * content emphasis). */
  font: 700 1.15rem var(--font-body);
  color: var(--accent-mine);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
/* Hero chevron rule odstraněn (V5 iter 4, Petřin pokyn 2026-05-28
 * "proč je tam ta šipka?"): chevron mezi jménem a × close vypadal
 * redundant + falešně signalizoval drill-down (hero otevírá Profile
 * modal, ne sub-screen). Profile řádek v ACCOUNT sekci je explicit
 * entry; hero je identity display + tichý tap shortcut bez affordance
 * indikátoru (iOS Settings hero pattern). */

/* Auth slot (anon Sign in / Sign up CTAs) — vertical rhythm only.
 * NO bottom hairline (Petřin pokyn 2026-06-01): the slot is only ever
 * populated for anon users (signed-in identity goes to the hero card), and
 * for anon the nav below is empty, so this border sat right next to the
 * legal-footer hairline = two stacked dividers. The footer hairline alone
 * separates the CTAs from About/Privacy/Terms. Signed-in users are
 * unaffected (empty slot → rule never matches); their hero→Account
 * divider (.app-menu__divider) and footer hairline stay. */
.app-menu .app-menu__auth:not(:empty) {
  padding: var(--space-2) 0 var(--space-3);
  margin-bottom: var(--space-2);
}
.app-menu .app-menu__user-badge {
  display: none;                    /* superseded by hero card */
}

/* Section divider + group label ("Account") — iOS/Android Settings
 * convention. Lowers cognitive load by labeling tier. */
.app-menu .app-menu__divider {
  border: 0;
  border-top: 1px solid var(--border-on-light);
  margin: var(--space-2) 0 0;
}
.app-menu .app-menu__group-label {
  font: 600 0.72rem var(--font-body);
  letter-spacing: 0.18em;
  text-transform: uppercase;
  /* Brighter than --text-light-muted (#8c95b8) so the section header
   * reads on dark glass background — Petřin V4 review flagged muted
   * footer text as illegible, same color tier would repeat the bug. */
  color: rgba(238, 240, 250, 0.72);
  padding: var(--space-3) var(--space-2) var(--space-1);
}

/* Nav list — Security / Preferences / Your data. NO chevrons (they
 * open modals, not drill-down screens — chevron would be a false
 * affordance). Icon + label, single teal tone matching desktop. */
.app-menu .app-menu__nav {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: 2px;
  border-top: 0;
}
.app-menu .app-menu__nav li {
  list-style: none;
}
.app-menu .app-menu__nav button,
.app-menu__nav-section--admin a {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  width: 100%;
  min-height: 52px;
  padding: var(--space-3);
  color: var(--text-dark);
  background: transparent;
  border: 0;
  font: inherit;
  font-size: 1rem;
  text-align: left;
  text-decoration: none;
  border-radius: var(--radius-md);
  cursor: pointer;
  transition: background 120ms ease, transform 100ms ease;
}
.app-menu .app-menu__nav button:hover,
.app-menu .app-menu__nav button:focus-visible {
  background: rgba(var(--accent-turquoise-rgb), 0.10);
  outline: none;
}
.app-menu .app-menu__nav button:active {
  background: rgba(var(--accent-turquoise-rgb), 0.18);
  transform: scale(0.99);
}
.app-menu__nav-icon {
  flex-shrink: 0;
  width: 22px;
  height: 22px;
  color: var(--accent-turquoise);   /* single teal tone — matches desktop */
  transition: color 160ms ease;
}

/* Backstage admin card — copper border + ADMIN chip with dark text on
 * bright copper background (high WCAG contrast, readable independent
 * of surrounding glow tint). */
.app-menu__nav-section--admin a {
  background: linear-gradient(135deg,
    rgba(var(--accent-copper-rgb), 0.20),
    rgba(var(--accent-copper-rgb), 0.06));
  border: 1px solid rgba(var(--accent-copper-rgb), 0.40);
  color: var(--accent-copper-bright, var(--accent-copper));
  font-weight: 600;
  margin: var(--space-3) 0 var(--space-1);
  min-height: 56px;
}
.app-menu__nav-section--admin .app-menu__nav-icon {
  color: var(--accent-copper-bright, var(--accent-copper));
}
.app-menu__nav-section--admin a:hover,
.app-menu__nav-section--admin a:focus-visible {
  background: linear-gradient(135deg,
    rgba(var(--accent-copper-rgb), 0.32),
    rgba(var(--accent-copper-rgb), 0.12));
  outline: none;
}
.app-menu__nav-section--admin a:active {
  background: linear-gradient(135deg,
    rgba(var(--accent-copper-rgb), 0.40),
    rgba(var(--accent-copper-rgb), 0.18));
  transform: scale(0.99);
}
/* ADMIN chip odstraněn (V5 iter 3, Petřin pokyn 2026-05-28 "do Backstage
 * nemá přístup jen admin"): chip byl hardcoded "Admin" string, ale per
 * permissionsClient.hasAnyPermission() je Backstage gate splněn pro
 * admins + moderators + testers. Single label by lhal o roli userů.
 * Copper highlight Backstage rowu už dává dostatečný visual emphasis,
 * chip není potřeba. CSS rule odstraněn — markup chip span v indexu
 * také odebrán. */

/* Sign out — destructive (red), subdued size (rare action, never
 * primary). Per feedback_destructive_ctas_uniform_red.md. */
.app-menu__nav-section--signout button {
  color: var(--danger-text);
  font-weight: 500;
  font-size: 0.95rem;
  min-height: 48px;
  margin-top: var(--space-1);
}
.app-menu__nav-section--signout .app-menu__nav-icon {
  color: var(--danger-text);
  width: 20px;
  height: 20px;
}
.app-menu__nav-section--signout button:hover,
.app-menu__nav-section--signout button:focus-visible {
  background: rgba(217, 107, 107, 0.10);
}
.app-menu__nav-section--signout button:active {
  background: rgba(217, 107, 107, 0.18);
  transform: scale(0.99);
}

/* Legal footer — About / Privacy / Terms. Vráceno do menu (Petřin
 * pokyn 2026-05-28 "kde je privacy, terms a podobně"): perma
 * .app-footer je na mobile display:none, takže vyhození z menu by
 * odebralo discoverability. Drží VEN z Account groupu (vlastní hairline
 * + center alignment + menší font), ale brighter kontrast než V4 muted
 * token — V4 footer kontrast bug se neopakuje. */
.app-menu .app-menu__footer-bar {
  display: flex;
  gap: var(--space-4);
  justify-content: center;
  padding-top: var(--space-3);
  margin-top: var(--space-3);
  border-top: 1px solid var(--border-on-light);
}
.app-menu .app-menu__footer-bar a {
  /* Brighter than --text-light-muted (#8c95b8) to fix V4 illegibility. */
  font-size: 0.85rem;
  font-weight: 500;
  color: rgba(238, 240, 250, 0.78);
  text-decoration: none;
  padding: var(--space-1-5) var(--space-2);
  border-radius: var(--radius-md);
  transition: color 160ms ease, background 160ms ease;
}
.app-menu .app-menu__footer-bar a:hover,
.app-menu .app-menu__footer-bar a:focus-visible {
  color: var(--text-dark);
  background: rgba(238, 240, 250, 0.06);
  outline: none;
}
.app-menu .app-menu__footer-bar a:active {
  background: rgba(238, 240, 250, 0.12);
}

/* ── 19. .fm-action-sheet — MO-4 unified kebab action sheet ──────
 * ONE reusable bottom-anchored sheet opened by the "⋮" in every detail
 * sheet. Sits at z-quick-menu (80), ABOVE the detail sheet (40) + tab bar
 * (45), so it overlays the detail it acts on. Shares the glass + tone-coded
 * icon language of .fm-quick-menu; differs only in being full-width and
 * slid up from the bottom edge (action-on-detail, not a floating popover).
 * Role tones are sacred: teal=edit · gold=suggest · magenta=report ·
 * danger(brick)=delete. */
.fm-action-sheet-backdrop {
  position: fixed;
  inset: 0;
  background: var(--m13-backdrop-color);
  opacity: 0;
  z-index: var(--m13-z-quick-menu);
  transition: opacity 200ms ease;
}
.fm-action-sheet-backdrop.is-open { opacity: 0.45; }

.fm-action-sheet {
  position: fixed;
  left: 0;
  right: 0;
  bottom: 0;
  z-index: calc(var(--m13-z-quick-menu) + 1);
  background: var(--glass-strong);
  -webkit-backdrop-filter: blur(20px) saturate(180%);
  backdrop-filter: blur(20px) saturate(180%);
  border-top: 1px solid var(--border-strong-light);
  border-radius: var(--m13-sheet-radius) var(--m13-sheet-radius) 0 0;
  box-shadow: 0 -18px 50px rgba(0, 0, 0, 0.60);
  color: var(--text-dark);
  padding: 6px 12px calc(env(safe-area-inset-bottom) + 12px);
  transform: translateY(100%);
  transition: transform 220ms cubic-bezier(0.22, 1, 0.36, 1);
}
.fm-action-sheet.is-open { transform: translateY(0); }

.fm-action-sheet__handle {
  display: grid;
  place-items: center;
  padding: 6px 0 8px;
  cursor: grab;
  touch-action: none;
}
.fm-action-sheet__handle-bar {
  width: 40px;
  height: 4px;
  border-radius: 999px;
  background: var(--border-strong-light);
}

.fm-action-sheet__head {
  /* Petřin pokyn 2026-05-28 "Actions skoro nevidím" — bumped from
     --text-light-muted to --text-muted (čitelnější bez disabled feel) +
     weight 600 + tighter tracking for visual weight. */
  padding: 4px 6px 10px;
  font: 600 11px var(--font-body);
  letter-spacing: 0.20em;
  text-transform: uppercase;
  color: var(--text-muted);
}

.fm-action-sheet__list {
  display: flex;
  flex-direction: column;
  gap: 2px;
}

.fm-action-sheet__item {
  width: 100%;
  display: flex;
  align-items: center;
  /* M12D.X-CTXMENU (Petřin výběr 2026-05-28, prototyp _m12d-ctxmenu-proto) —
     lighter rows: airier gap + padding, no boxed icon tile. Tap target stays
     ≥50px (P-041). */
  gap: 16px;
  padding: 13px 14px;
  min-height: 50px;
  background: transparent;
  border: 0;
  border-radius: 14px;
  cursor: pointer;
  text-align: left;
  color: var(--text-dark);
}
.fm-action-sheet__item:active { background: rgba(238, 240, 250, 0.05); }

/* M12D.X-CTXMENU — un-tiled glyph: just the colour-coded line icon (boxed
   tinted tiles were the main source of visual "weight"; the tone now lives in
   the icon stroke + the label, per mockup 05 + approved prototype). */
.fm-action-sheet__icon {
  width: 24px;
  height: 24px;
  display: grid;
  place-items: center;
  flex-shrink: 0;
  /* tone-coded icon colour per modifier below */
}
.fm-action-sheet__item--teal    .fm-action-sheet__icon { color: var(--accent-turquoise); }
.fm-action-sheet__item--gold    .fm-action-sheet__icon { color: var(--accent-copper); }
.fm-action-sheet__item--magenta .fm-action-sheet__icon { color: var(--accent-mine); }
.fm-action-sheet__item--danger  .fm-action-sheet__icon { color: var(--danger-text); }
/* M12D.X-REPORT-INDICATORS (Petřin pokyn 2026-05-30) — the reported moderate row
   MARKS its flag: a solid danger tile + halo + dark glyph, matching the kebab's
   flag badge so "reported" reads consistently from the trigger into the menu.
   The label stays danger-coloured (inherits from --danger above). */
.fm-action-sheet__item--reported .fm-action-sheet__icon {
  background: var(--danger);
  color: var(--text-on-accent);
  border-radius: 7px;
  box-shadow: var(--halo-danger);
}

.fm-action-sheet__label {
  font: 600 15.5px var(--font-body);
  letter-spacing: 0.005em;
  color: var(--text-dark);
}

/* Quiet right-aligned caption (e.g. "Community" on Suggest a change). */
.fm-action-sheet__meta {
  margin-left: auto;
  font: 600 10px var(--font-body);
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--text-light-muted);
}
/* Destructive row — brick label so the danger reads even before the icon. */
.fm-action-sheet__item--danger .fm-action-sheet__label { color: var(--danger-text); }

/* Divider before the destructive tail. */
.fm-action-sheet__divider {
  height: 1px;
  margin: 6px 8px;
  background: var(--border-on-light);
}

/* Cancel row — neutral, centred, no icon column. Petřin pokyn 2026-05-28
 * "Cancel skoro nevidím" — bumped label from --text-light-muted to
 * --text-muted + slightly stronger background so the row reads as actually
 * tappable, not disabled. */
.fm-action-sheet__item--cancel {
  justify-content: center;
  margin-top: 4px;
  background: rgba(238, 240, 250, 0.07);
}
.fm-action-sheet__item--cancel .fm-action-sheet__label { color: var(--text-muted); }

/* Disabled row — listed but inert + dimmed (BS-4 / m14 §6: the moderator
 * sees the action exists but learns it's currently unavailable). */
.fm-action-sheet__item--disabled {
  opacity: 0.45;
  cursor: not-allowed;
}

@media (prefers-reduced-motion: reduce) {
  .fm-action-sheet { transition: none; }
  .fm-action-sheet-backdrop { transition: none; }
}

/* =============================================================
 * MO-5 — FULLSCREEN FORMS (create / report / suggest / delete)
 *
 * On desktop these are floating <dialog> cards (440px, centred,
 * rounded, with a backdrop). On phone width they become FULLSCREEN
 * forms per the mockups (17/18/19): a sticky header (back arrow ·
 * centred title), a scrolling body of the SAME fields, and a sticky
 * full-width Save bar pinned above the keyboard + the iOS gesture
 * bar (safe-area-inset-bottom).
 *
 * This is a SURFACE reskin only — the existing form fields, validation,
 * Frost date/time pickers and submit handlers are untouched. We reuse
 * the visual language MO-1 shipped in `.fm-form-fullscreen*` (same
 * gradients / glass head + foot / tone-coded save), but applied to the
 * production dialog DOM (#pin-form-dialog, .event-create-dialog,
 * #report-form-dialog, .frost-modal) rather than a forked structure.
 *
 * Forms in scope + their tone:
 *   #pin-form-dialog            → moment   (magenta)
 *   #event-create-dialog        → concert  (teal)
 *   #online-event-create-dialog → online   (iris / violet)
 *   #poi-create-dialog          → landmark (gold / copper)
 *   #report-form-dialog         → neutral
 *   #change-suggestion-dialog   → neutral  (.frost-modal)
 *   #deletion-request-dialog    → danger   (.frost-modal)
 *   #tour-form-dialog           → neutral  (.frost-modal-ish; admin-only,
 *                                  styled defensively for consistency)
 *
 * Close policy is unchanged (modalUtils): back arrow / × / Esc close;
 * NO backdrop dismiss. The existing close button is re-skinned into the
 * top-left back arrow — it already calls dialog.close() (= "go back").
 * The two .frost-modal forms carry no close button (data-no-auto-close-
 * button), so they keep their footer Cancel button as the exit.
 * ============================================================= */

/* Shared selector list — the dialog shells we convert to fullscreen. */
#pin-form-dialog[open],
#event-create-dialog[open],
#online-event-create-dialog[open],
#poi-create-dialog[open],
#report-form-dialog[open],
#change-suggestion-dialog[open],
#deletion-request-dialog[open],
#tour-form-dialog[open],
#community-hub-dialog[open] {
  position: fixed;
  inset: 0;
  margin: 0;
  width: 100vw;
  max-width: 100vw;
  height: 100dvh;
  max-height: 100dvh;
  border: 0;
  border-radius: 0;
  padding: 0;
  overflow: hidden;
  color: var(--text-dark);
  /* Default surface = moment-neutral cream gradient; tone variants below
   * paint the radial accent wash matching the MO-1 fullscreen variants. */
  background: linear-gradient(180deg, var(--cream-surface) 0%, var(--cream-base) 100%);
}

/* Tone washes — mirror MO-1 .fm-form-fullscreen--{moment,concert,landmark}
 * and extend with online (iris) + delete (danger) per derived treatment. */
#pin-form-dialog[open] {
  background: radial-gradient(ellipse 80% 40% at 50% 0%, rgba(var(--accent-mine-rgb), 0.14) 0%, transparent 60%),
              linear-gradient(180deg, var(--cream-surface) 0%, var(--cream-base) 100%);
}
#event-create-dialog[open] {
  background: radial-gradient(ellipse 80% 40% at 50% 0%, rgba(var(--accent-turquoise-rgb), 0.10) 0%, transparent 60%),
              linear-gradient(180deg, var(--cream-surface) 0%, var(--cream-base) 100%);
}
#online-event-create-dialog[open] {
  background: radial-gradient(ellipse 80% 40% at 50% 0%, rgba(var(--accent-online-rgb), 0.12) 0%, transparent 60%),
              linear-gradient(180deg, var(--cream-surface) 0%, var(--cream-base) 100%);
}
#poi-create-dialog[open] {
  background: radial-gradient(ellipse 80% 40% at 50% 0%, rgba(var(--accent-copper-rgb), 0.12) 0%, transparent 60%),
              linear-gradient(180deg, var(--cream-surface) 0%, var(--cream-base) 100%);
}
#deletion-request-dialog[open] {
  background: radial-gradient(ellipse 80% 40% at 50% 0%, rgba(var(--danger-rgb), 0.12) 0%, transparent 60%),
              linear-gradient(180deg, var(--cream-surface) 0%, var(--cream-base) 100%);
}
/* Community hub (ADR-252) — teal wash (the hub's world tone) + its own
 * column layout: header (sticky) · body (the hub bands; feed scrolls). */
#community-hub-dialog[open] {
  display: flex;
  flex-direction: column;
  background: radial-gradient(ellipse 80% 40% at 50% 0%, rgba(var(--accent-turquoise-rgb), 0.12) 0%, transparent 60%),
              linear-gradient(180deg, var(--cream-surface) 0%, var(--cream-base) 100%);
}

/* The backdrop is invisible at fullscreen (the form fills everything),
 * but keep it opaque-ink so nothing shows through during the open/close
 * frame. */
#pin-form-dialog[open]::backdrop,
#event-create-dialog[open]::backdrop,
#online-event-create-dialog[open]::backdrop,
#poi-create-dialog[open]::backdrop,
#report-form-dialog[open]::backdrop,
#change-suggestion-dialog[open]::backdrop,
#deletion-request-dialog[open]::backdrop,
#tour-form-dialog[open]::backdrop,
#community-hub-dialog[open]::backdrop {
  background: var(--cream-base);
  -webkit-backdrop-filter: none;
  backdrop-filter: none;
}

/* The <form> is the flex column shell: head (sticky) · body (scroll) ·
 * footer (sticky). The pin-form already nests its scroller in
 * .pin-form__body; the event-create / frost-modal forms scroll the form
 * itself. We normalise both to a column flexbox that fills the dialog. */
#pin-form-dialog[open] > #pin-form,
#event-create-dialog[open] > .event-create__form,
#online-event-create-dialog[open] > .event-create__form,
#poi-create-dialog[open] > .event-create__form,
#report-form-dialog[open] > #report-form,
#change-suggestion-dialog[open] > #change-suggestion-form,
#deletion-request-dialog[open] > #deletion-request-form,
#tour-form-dialog[open] > form {
  display: flex;
  flex-direction: column;
  height: 100dvh;
  max-height: 100dvh;
  min-height: 0;
  margin: 0;
  padding: 0;
  overflow: hidden;
}

/* ── Sticky header — back arrow (left) · centred title · spacer ──
 * .event-create__header is already a block; the pin-form has its own
 * .pin-form__header; report / frost-modal forms have a bare <h2>. We
 * give them all the same glass sticky bar. */
#pin-form-dialog[open] .pin-form__header,
#event-create-dialog[open] .event-create__header,
#online-event-create-dialog[open] .event-create__header,
#poi-create-dialog[open] .event-create__header,
#community-hub-dialog[open] .event-create__header {
  position: sticky;
  top: 0;
  z-index: 10;
  flex: 0 0 auto;
  margin: 0;
  padding: calc(env(safe-area-inset-top) + 8px) 14px 12px;
  background: linear-gradient(180deg, var(--glass-strong) 75%, transparent);
  -webkit-backdrop-filter: blur(14px);
  backdrop-filter: blur(14px);
  /* Reserve the same 44px gutter on both sides so the title centres
   * between the back arrow (left) and an equal spacer (right). */
  display: flex;
  flex-direction: row;
  align-items: center;
  min-height: 56px;
  text-align: center;
}

/* Centred italic display title. Matches the canonical dialog-title size
   (italic 700 1.55rem Cormorant) used by every non-sticky dialog (e.g. the
   Add-Pin chooser) + the desktop header — the old italic-600-18px shrank it
   well below "our standard" on mobile (Petřin pokyn 2026-05-31). */
#pin-form-dialog[open] .pin-form__header h2,
#event-create-dialog[open] .event-create__header h2,
#online-event-create-dialog[open] .event-create__header h2,
#poi-create-dialog[open] .event-create__header h2,
#community-hub-dialog[open] .event-create__header h2 {
  flex: 1;
  order: 2;            /* between back arrow (order 1) + spacer (order 3) */
  margin: 0;
  padding: 0 4px;
  text-align: center;
  font: italic 700 1.55rem var(--font-display);
  color: var(--text-dark);
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* event-create wraps its h2 in a .title-row with a "+" badge; flatten the
 * row so the title centres and drop the badge (the tone wash + save colour
 * already encode the form's identity). The subtitle moves into the body. */
#event-create-dialog[open] .event-create__title-row,
#online-event-create-dialog[open] .event-create__title-row,
#poi-create-dialog[open] .event-create__title-row {
  flex: 1;
  order: 2;
  display: flex;
  justify-content: center;
  align-items: center;
  min-width: 0;
  margin: 0;
}
#event-create-dialog[open] .event-create__title-badge,
#online-event-create-dialog[open] .event-create__title-badge,
#poi-create-dialog[open] .event-create__title-badge {
  display: none;
}
/* Subtitle lives inside the header and would break the single-row centred
 * title, so hide it on phone. The paste-field label already explains the
 * link-first flow, so no information is lost. */
#event-create-dialog[open] .event-create__subtitle,
#online-event-create-dialog[open] .event-create__subtitle,
#poi-create-dialog[open] .event-create__subtitle {
  display: none;
}

/* Bare <h2> heading inside report / frost-modal forms → sticky title bar.
 * These have no header wrapper, so the <h2> itself becomes the bar. */
#report-form-dialog[open] > #report-form > h2,
#change-suggestion-dialog[open] .frost-modal__title,
#deletion-request-dialog[open] .frost-modal__title {
  position: sticky;
  top: 0;
  z-index: 10;
  flex: 0 0 auto;
  margin: 0;
  padding: calc(env(safe-area-inset-top) + 8px) 56px 12px;
  background: linear-gradient(180deg, var(--glass-strong) 75%, transparent);
  -webkit-backdrop-filter: blur(14px);
  backdrop-filter: blur(14px);
  min-height: 56px;
  display: flex;
  align-items: center;
  justify-content: center;
  text-align: center;
  font: italic 700 1.55rem var(--font-display);   /* match canonical dialog title (was 18px) */
  color: var(--text-dark);
}

/* ── Back arrow — reskin the existing close button (top-left, circular,
 * glyph swapped × → ←). It already triggers dialog.close() so the
 * semantics ("go back / cancel") are correct with zero JS. */
#pin-form-dialog[open] .pin-form__close,
#event-create-dialog[open] .dialog-close,
#online-event-create-dialog[open] .dialog-close,
#poi-create-dialog[open] .dialog-close,
#report-form-dialog[open] .dialog-close,
#community-hub-dialog[open] .dialog-close {
  position: relative;     /* was absolute top-right; pull into header flow */
  order: 1;
  top: auto;
  right: auto;
  left: auto;
  width: 40px;
  height: 40px;
  min-width: 40px;
  font-size: 0;           /* hide the original × text node */
  flex: 0 0 auto;
  z-index: 11;
  /* Plain teal chevron, NO disc (Petřin pokyn 2026-06-01 "žádná vyplněná
   * elipsa kolem") — match the canonical .fm-sheet__back back arrow. The
   * 2026-05-26 glass-disc treatment is superseded. */
  background: transparent;
  border: 0;
  box-shadow: none;
  -webkit-backdrop-filter: none;
  backdrop-filter: none;
  color: var(--accent-turquoise);
  transition: color 160ms ease, transform 120ms ease;
}
#pin-form-dialog[open] .pin-form__close:active,
#event-create-dialog[open] .dialog-close:active,
#online-event-create-dialog[open] .dialog-close:active,
#poi-create-dialog[open] .dialog-close:active,
#report-form-dialog[open] .dialog-close:active,
#community-hub-dialog[open] .dialog-close:active {
  color: var(--accent-turquoise-bright);
  transform: scale(0.94);
}
/* Swap the glyph: the × lives in a child <span aria-hidden> (event-create /
 * poi / report) or as direct text (pin-form). Hide any child + paint a ←
 * via ::before so we never touch the DOM. */
#pin-form-dialog[open] .pin-form__close > *,
#event-create-dialog[open] .dialog-close > *,
#online-event-create-dialog[open] .dialog-close > *,
#poi-create-dialog[open] .dialog-close > *,
#report-form-dialog[open] .dialog-close > *,
#community-hub-dialog[open] .dialog-close > * {
  display: none;
}
#pin-form-dialog[open] .pin-form__close::before,
#event-create-dialog[open] .dialog-close::before,
#online-event-create-dialog[open] .dialog-close::before,
#poi-create-dialog[open] .dialog-close::before,
#report-form-dialog[open] .dialog-close::before,
#community-hub-dialog[open] .dialog-close::before {
  /* Same chevron glyph as the canonical .fm-sheet__back (SVG path masked
     in the button's teal colour) — one back-arrow shape app-wide. */
  content: "";
  display: block;
  width: 22px;
  height: 22px;
  background: currentColor;
  -webkit-mask: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="black" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M15 6l-6 6 6 6"/></svg>') center / 22px no-repeat;
  mask: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="black" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M15 6l-6 6 6 6"/></svg>') center / 22px no-repeat;
}

/* For report, the bare-<h2> bar has padding-left:56px reserving the gutter;
 * the injected .dialog-close (absolute by default) sits in that gutter. */
#report-form-dialog[open] .dialog-close {
  position: absolute;
  top: calc(env(safe-area-inset-top) + 12px);
  left: 12px;
  order: 0;
}

/* Right-side spacer keeps the header title optically centred opposite the
 * 40px back arrow (event-create / pin-form rows). Injected via ::after on
 * the header so we add no DOM. */
#pin-form-dialog[open] .pin-form__header::after,
#event-create-dialog[open] .event-create__header::after,
#online-event-create-dialog[open] .event-create__header::after,
#poi-create-dialog[open] .event-create__header::after,
#community-hub-dialog[open] .event-create__header::after {
  content: "";
  order: 3;
  width: 40px;
  flex: 0 0 auto;
}

/* ── Community hub fullscreen specifics (ADR-252, mockup 04-mobile) ──
 * The hub isn't a <form>: header (sticky, above) · .community-hub__body
 * is the flex column — canonical card pinned, feed scrolls, the share
 * band is a sticky bottom bar over the safe area. The h2 sits between
 * the back chevron (order 1) and the ::after spacer (order 3). */
#community-hub-dialog[open] .event-create__header h2 {
  order: 2;
}
#community-hub-dialog[open] .dialog-hint {
  display: none; /* subtitle is desktop-band content; mobile keeps the bar clean */
}
#community-hub-dialog[open] .community-hub__body {
  flex: 1 1 auto;
  min-height: 0;
  height: auto;          /* desktop fixes the height; here the dialog does */
  display: flex;
  flex-direction: column;
  padding: 0 14px;
}
#community-hub-dialog[open] .hub-canon {
  margin-top: 6px;
}
#community-hub-dialog[open] .hub-feed {
  -webkit-mask-image: none;
  mask-image: none;      /* the sticky add bar already hints the cut-off */
}
#community-hub-dialog[open] .hub-add {
  margin: 0 -14px;
  padding: 12px 16px calc(12px + env(safe-area-inset-bottom));
  background: linear-gradient(180deg, rgba(13, 18, 38, 0), rgba(13, 18, 38, 0.96) 26%);
  -webkit-backdrop-filter: blur(14px);
  backdrop-filter: blur(14px);
}
/* (Kebab actions are always visible at --text-muted - feed parity,
 * Petrin pokyn 2026-06-04 vecer; no per-device opacity needed.) */

/* M12 gathering title fix (Petřin pokyn 2026-05-31) — the title h2 is wrapped in
 * a .pin-form__header-text column. On mobile the header is the centred title bar,
 * so that wrapper takes the centred middle slot (between back arrow order 1 and
 * the ::after spacer order 3) and centres the title. The gathering identity now
 * lives in a body card (proposal A), so NO gathering rules belong in the header. */
#pin-form-dialog[open] .pin-form__header-text {
  order: 2;
  flex: 1 1 auto;
  min-width: 0;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
}
/* The h2 sits inside the wrapper now — neutralise the shared flex:1 (would stretch
 * it vertically inside the column); keep the single-line ellipsis from above. */
#pin-form-dialog[open] .pin-form__header .pin-form__header-text h2 {
  flex: 0 0 auto;
  max-width: 100%;
}
/* The desktop header mesh fights the mobile glass bar — keep the glass. */
#pin-form-dialog[open] .pin-form__header {
  border-bottom: none;
}

/* ── Scrolling body — everything between header and footer. The pin-form
 * already has .pin-form__body; the others scroll the form, so we make the
 * field area grow + scroll while head/foot stay pinned. For event-create /
 * report / frost forms the fields are direct children of the <form>; we
 * can't wrap them, so we let the form scroll and pin only head + foot via
 * position:sticky (head) + the footer rule below. */
#pin-form-dialog[open] .pin-form__body {
  flex: 1 1 auto;
  min-height: 0;
  overflow-y: auto;
  -webkit-overflow-scrolling: touch;
  padding: 14px 14px 8px;
}
/* event-create / report / frost: the <form> itself is the scroller. Header
 * is sticky-top, footer sticky-bottom, the field flow scrolls between them.
 * Give the form horizontal padding + scroll. */
#event-create-dialog[open] > .event-create__form,
#online-event-create-dialog[open] > .event-create__form,
#poi-create-dialog[open] > .event-create__form,
#report-form-dialog[open] > #report-form,
#change-suggestion-dialog[open] > #change-suggestion-form,
#deletion-request-dialog[open] > #deletion-request-form,
#tour-form-dialog[open] > form {
  overflow-y: auto;
  -webkit-overflow-scrolling: touch;
}
/* Side padding for fields in the scrolling forms (head + foot supply their
 * own padding; the field flow needs gutters). */
#event-create-dialog[open] > .event-create__form > :not(.event-create__header):not(.event-create__actions),
#online-event-create-dialog[open] > .event-create__form > :not(.event-create__header):not(.event-create__actions),
#poi-create-dialog[open] > .event-create__form > :not(.event-create__header):not(.event-create__actions),
#report-form-dialog[open] > #report-form > :not(h2):not(.form-actions),
#change-suggestion-dialog[open] > #change-suggestion-form > :not(.frost-modal__title):not(.form-actions),
#deletion-request-dialog[open] > #deletion-request-form > :not(.frost-modal__title):not(.form-actions),
#tour-form-dialog[open] > form > :not(.event-create__header):not(.event-create__actions):not(.form-actions) {
  margin-left: 14px;
  margin-right: 14px;
}

/* ── Sticky Save bar — full-width primary, pinned above the keyboard +
 * the iOS gesture bar. The existing footer (.form-actions /
 * .event-create__actions) becomes the bar. */
#pin-form-dialog[open] > #pin-form > .form-actions,
#event-create-dialog[open] .event-create__actions,
#online-event-create-dialog[open] .event-create__actions,
#poi-create-dialog[open] .event-create__actions,
#report-form-dialog[open] .form-actions,
#change-suggestion-dialog[open] .form-actions,
#deletion-request-dialog[open] .form-actions,
#tour-form-dialog[open] .form-actions {
  position: sticky;
  bottom: 0;
  z-index: 10;
  flex: 0 0 auto;
  margin: 0;
  /* Short forms don't overflow the 100dvh flex column, so sticky alone
     leaves the bar floating right under the last field (and it jumps as
     autofill grows the content — Petra 2026-06-06, landmark form). Auto
     top margin pins it to the screen bottom; once content overflows, the
     margin resolves to 0 and sticky takes over unchanged. */
  margin-top: auto;
  padding: 12px 14px calc(env(safe-area-inset-bottom) + 18px);
  background: linear-gradient(180deg, transparent 0%, var(--glass-strong) 30%);
  -webkit-backdrop-filter: blur(14px);
  backdrop-filter: blur(14px);
  /* Stack: full-width primary; ghost cancel (when kept) sits below it. */
  display: flex;
  flex-direction: column-reverse;
  gap: 10px;
}

/* Full-width primary submit, tone-coded gradient + glow per form. The
 * existing submit keeps its id/class/handler; we only resize + repaint. */
#pin-form-dialog[open] #pin-form-submit,
#event-create-dialog[open] .event-create__actions button[type="submit"],
#online-event-create-dialog[open] .event-create__actions button[type="submit"],
#poi-create-dialog[open] .event-create__actions button[type="submit"],
#report-form-dialog[open] .form-actions button[type="submit"],
#change-suggestion-dialog[open] .form-actions button[type="submit"],
#deletion-request-dialog[open] .form-actions button[type="submit"],
#tour-form-dialog[open] .form-actions button[type="submit"] {
  width: 100%;
  margin: 0;
  padding: 15px;
  border-radius: 14px;
  border: 0;
  color: var(--text-on-accent);
  font: 700 var(--fs-md, 14px) var(--font-body);
  letter-spacing: 0.04em;
}
#pin-form-dialog[open] #pin-form-submit:disabled,
#event-create-dialog[open] .event-create__actions button[type="submit"]:disabled,
#online-event-create-dialog[open] .event-create__actions button[type="submit"]:disabled,
#poi-create-dialog[open] .event-create__actions button[type="submit"]:disabled,
#report-form-dialog[open] .form-actions button[type="submit"]:disabled,
#change-suggestion-dialog[open] .form-actions button[type="submit"]:disabled,
#deletion-request-dialog[open] .form-actions button[type="submit"]:disabled {
  opacity: 0.5;
  cursor: not-allowed;
}

/* Tone-coded gradient + glow per variant. */
/* ADR-236 (Petřin pokyn 2026-06-03): SUBMIT = TEAL everywhere — the magenta
   moment-tone stays on special actions (map "+ MOMENT" FAB, join-gathering),
   never on a form save button (HARD RULE 12 + designer echo-modal handoff). */
#pin-form-dialog[open] #pin-form-submit {
  background: linear-gradient(180deg, var(--accent-turquoise), var(--accent-turquoise-deep));
  box-shadow: 0 0 20px rgba(var(--accent-turquoise-rgb), 0.33), 0 6px 18px rgba(0, 0, 0, 0.40);
}
#event-create-dialog[open] .event-create__actions button[type="submit"] {
  background: linear-gradient(180deg, var(--accent-turquoise), var(--accent-turquoise-deep));
  box-shadow: 0 0 20px rgba(var(--accent-turquoise-rgb), 0.33), 0 6px 18px rgba(0, 0, 0, 0.40);
}
#online-event-create-dialog[open] .event-create__actions button[type="submit"] {
  background: linear-gradient(180deg, var(--accent-online), var(--accent-online-deep));
  box-shadow: 0 0 20px rgba(var(--accent-online-rgb), 0.33), 0 6px 18px rgba(0, 0, 0, 0.40);
}
#poi-create-dialog[open] .event-create__actions button[type="submit"] {
  background: linear-gradient(180deg, var(--accent-copper), var(--accent-copper-deep));
  box-shadow: 0 0 20px rgba(var(--accent-copper-rgb), 0.33), 0 6px 18px rgba(0, 0, 0, 0.40);
}
#report-form-dialog[open] .form-actions button[type="submit"],
#change-suggestion-dialog[open] .form-actions button[type="submit"],
#tour-form-dialog[open] .form-actions button[type="submit"] {
  background: linear-gradient(180deg, var(--accent-turquoise), var(--accent-turquoise-deep));
  box-shadow: 0 0 20px rgba(var(--accent-turquoise-rgb), 0.33), 0 6px 18px rgba(0, 0, 0, 0.40);
}
/* Deletion request = destructive, brick tone. */
#deletion-request-dialog[open] .form-actions button[type="submit"] {
  color: var(--text-light);
  background: linear-gradient(180deg, var(--danger), var(--danger-strong, var(--danger)));
  box-shadow: 0 0 20px rgba(var(--danger-rgb), 0.33), 0 6px 18px rgba(0, 0, 0, 0.40);
}

/* Cancel / ghost button in the save bar. For forms WITH a back arrow
 * (pin-form, event-create, online, poi, report) the back arrow already
 * handles cancel → hide the redundant ghost button (mockup: no ghost
 * cancel). For the two .frost-modal forms (suggest, delete) there is NO
 * header close button, so KEEP their Cancel as the only exit. */
#pin-form-dialog[open] > #pin-form > .form-actions > #form-cancel,
#event-create-dialog[open] .event-create__actions > button[type="button"],
#online-event-create-dialog[open] .event-create__actions > button[type="button"],
#poi-create-dialog[open] .event-create__actions > button[type="button"],
#report-form-dialog[open] .form-actions > button[type="button"] {
  display: none;
}
/* Kept Cancel buttons (frost-modal) — full-width ghost below the Save. */
#change-suggestion-dialog[open] .form-actions > button[type="button"],
#deletion-request-dialog[open] .form-actions > button[type="button"],
#tour-form-dialog[open] .form-actions > button[type="button"] {
  width: 100%;
  margin: 0;
  padding: 13px;
  border-radius: 14px;
}

/* ── Field rhythm — give the scrolling field rows comfortable vertical
 * spacing so the form breathes at fullscreen (mockup cadence). Targets the
 * shared .field primitive used across all these forms. */
#pin-form-dialog[open] .pin-form__body > .field,
#pin-form-dialog[open] .pin-form__body > .echo-group,
#pin-form-dialog[open] .pin-form__body > .echo-grouping-field:not([hidden]),
#pin-form-dialog[open] .pin-form__body > .pin-form__location,
#pin-form-dialog[open] .pin-form__body > .profile-identity-row,
#event-create-dialog[open] .event-create__form > .field,
#online-event-create-dialog[open] .event-create__form > .field,
#poi-create-dialog[open] .event-create__form > .field,
#report-form-dialog[open] #report-form > .field,
#change-suggestion-dialog[open] .frost-modal__label,
#deletion-request-dialog[open] .frost-modal__label {
  margin-bottom: 16px;
}

/* ── 🔴 SCROLL FIX — header is a SIBLING of the form ─────────────
 * For event-create / online / poi the dialog DOM is:
 *     <dialog> <header>…</header> <form>…fields… <footer></form> </dialog>
 * i.e. the header is a DIRECT CHILD of the dialog, NOT inside the form.
 * The earlier rules made the FORM the flex column at height:100dvh — but
 * with the 56px header sitting ABOVE it inside an overflow:hidden 100dvh
 * dialog, the form overflowed the dialog by the header's height. Its last
 * fields + the sticky Save bar were pushed below the fold and CLIPPED →
 * unreachable (Petra 2026-05-26: "Add concert won't scroll to the Public
 * toggle"). Fix: make the DIALOG the flex column; header = fixed top row;
 * form = the flex:1 scroller between the header and its own sticky footer.
 * (pin-form / report / frost forms keep the header/h2 INSIDE the scroller,
 * so they were never broken and are untouched.) */
#event-create-dialog[open],
#online-event-create-dialog[open],
#poi-create-dialog[open] {
  display: flex;
  flex-direction: column;
}
#event-create-dialog[open] > .event-create__header,
#online-event-create-dialog[open] > .event-create__header,
#poi-create-dialog[open] > .event-create__header {
  position: static;   /* was sticky — it is a real flex row now */
  flex: 0 0 auto;
}
#event-create-dialog[open] > .event-create__form,
#online-event-create-dialog[open] > .event-create__form,
#poi-create-dialog[open] > .event-create__form {
  flex: 1 1 auto;     /* grow to fill between header and footer */
  height: auto;       /* override the earlier height:100dvh */
  max-height: none;
  min-height: 0;      /* allow the flex child to actually scroll */
}


/* =============================================================
 * MO-6 — FULLSCREEN AUTH & SETTINGS
 *
 * On desktop the auth + account dialogs are floating <dialog>
 * cards (440px, centred, rounded — see style.css dialog[id^="auth-"]).
 * On phone width they become FULLSCREEN steps per mockups 25/26
 * (sign-in / sign-up) and the same treatment derived for the rest:
 * a sticky glass header (back arrow · centred title), a scrolling
 * body of the SAME fields, and a sticky full-width primary button
 * pinned above the keyboard + iOS gesture bar.
 *
 * This is a SURFACE reskin only — the field markup, validation,
 * Cognito/MFA submit handlers, typed-confirm delete safeguard and
 * close policy (modalUtils: back arrow / × / Esc; NO backdrop) are
 * all untouched. We mirror the MO-5 fullscreen-form chrome applied
 * to the production auth dialog DOM (dialog[id^="auth-"]) rather than
 * the handoff's forked `.fm-auth` shell (K1 — app shell is canonical
 * per ADR-179; we borrow only the visual treatment).
 *
 * Auth-entry CTAs (Sign in / Sign up / Create account / Send code /
 * Reset password / Confirm) keep their existing COPPER gradient from
 * style.css §dialog[id^="auth-"] button[type=submit]. Settings Save
 * (Profile / Preferences) keeps its existing teal .btn-primary — the
 * app's per-role Save colour. Field labels stay the shared
 * `--text-muted` token. No new tone tokens introduced.
 *
 * The avatar-picker dialog is intentionally NOT fullscreened here —
 * it is a sub-picker launched from Profile and keeps its centred
 * card (its own .dialog-close already handles dismissal).
 * ============================================================= */

/* ADR-189 Stage 4 — anonymous avatar picker on small screens: a touch more
   breathing room (5-col sprite grid) + a taller scroll body so both sets are
   reachable above the bottom URL bar. Stays a centred Frost card. */
.avatar-picker-dialog--sets {
  width: min(94vw, 420px);
}
.avatar-picker-dialog--sets .avatar-grid.avatar-grid--sprite {
  grid-template-columns: repeat(4, 1fr);
}
.avatar-picker-dialog--sets .avatar-picker__body {
  max-height: 62dvh;
}

/* Shared selector list — every auth/settings dialog we fullscreen.
 * (avatar-picker excluded: kept as a centred sub-picker card.) */
#auth-login-dialog[open],
#auth-register-dialog[open],
#auth-verify-dialog[open],
#auth-forgot-email-dialog[open],
#auth-forgot-reset-dialog[open],
#auth-federated-consent-dialog[open],
#auth-delete-confirm-dialog[open],
#auth-mfa-setup-dialog[open],
#auth-mfa-disable-dialog[open],
#auth-mfa-challenge-dialog[open],
#auth-profile-dialog[open],
#auth-preferences-dialog[open],
#auth-security-dialog[open],
#auth-data-dialog[open] {
  position: fixed;
  inset: 0;
  margin: 0;
  width: 100vw;
  max-width: 100vw;
  height: 100dvh;
  max-height: 100dvh;
  border: 0;
  border-radius: 0;
  padding: 0;
  overflow: hidden;
  /* Copper radial wash (auth tone) over the cream→base gradient —
   * same intensity as the MO-5 landmark wash (0.12). */
  background: radial-gradient(ellipse 80% 40% at 50% 0%, rgba(var(--accent-copper-rgb), 0.12) 0%, transparent 60%),
              linear-gradient(180deg, var(--cream-surface) 0%, var(--cream-base) 100%);
  color: var(--text-dark);
  /* Reset the desktop flex-column gap (style.css dialog[id^="auth-"][open]);
   * the inner shell owns its own spacing at fullscreen. */
  gap: 0;
}

/* Danger tone for the delete-account step — brick wash, not copper. */
#auth-delete-confirm-dialog[open] {
  background: radial-gradient(ellipse 80% 40% at 50% 0%, rgba(var(--danger-rgb), 0.12) 0%, transparent 60%),
              linear-gradient(180deg, var(--cream-surface) 0%, var(--cream-base) 100%);
}

/* Opaque-ink backdrop so nothing shows through the open/close frame. */
#auth-login-dialog[open]::backdrop,
#auth-register-dialog[open]::backdrop,
#auth-verify-dialog[open]::backdrop,
#auth-forgot-email-dialog[open]::backdrop,
#auth-forgot-reset-dialog[open]::backdrop,
#auth-federated-consent-dialog[open]::backdrop,
#auth-delete-confirm-dialog[open]::backdrop,
#auth-mfa-setup-dialog[open]::backdrop,
#auth-mfa-disable-dialog[open]::backdrop,
#auth-mfa-challenge-dialog[open]::backdrop,
#auth-profile-dialog[open]::backdrop,
#auth-preferences-dialog[open]::backdrop,
#auth-security-dialog[open]::backdrop,
#auth-data-dialog[open]::backdrop {
  background: var(--cream-base);
  -webkit-backdrop-filter: none;
  backdrop-filter: none;
}

/* ── Flex column shell ──────────────────────────────────────────
 * Family A (form-wrapped): the <form> is the only flex child of the
 * dialog and carries h2 + fields + .form-actions — make IT the column.
 * Family B (Profile / Security / Data): the dialog itself is the
 * column (h2 + form/div + close-actions as direct children) — it is
 * already display:flex from style.css; we only need it to scroll. */
#auth-login-dialog[open] > #auth-login-form,
#auth-register-dialog[open] > #auth-register-form,
#auth-verify-dialog[open] > #auth-verify-form,
#auth-forgot-email-dialog[open] > #auth-forgot-email-form,
#auth-forgot-reset-dialog[open] > #auth-forgot-reset-form,
#auth-federated-consent-dialog[open] > #auth-federated-consent-form,
#auth-delete-confirm-dialog[open] > #auth-delete-confirm-form,
#auth-mfa-setup-dialog[open] > #auth-mfa-setup-form,
#auth-mfa-disable-dialog[open] > #auth-mfa-disable-form,
#auth-mfa-challenge-dialog[open] > #auth-mfa-challenge-form {
  display: flex;
  flex-direction: column;
  height: 100dvh;
  max-height: 100dvh;
  min-height: 0;
  margin: 0;
  padding: 0;
  overflow: hidden;
}

/* ── Sticky header — the bare <h2> becomes a centred glass title bar.
 * 56px tall, 56px left/right gutters so the back arrow (left) and the
 * optical spacer (right) leave the title centred. Mirrors MO-5's
 * bare-<h2> header treatment for report / frost-modal forms. */
#auth-verify-dialog[open] #auth-verify-form > h2,
#auth-forgot-email-dialog[open] #auth-forgot-email-form > h2,
#auth-forgot-reset-dialog[open] #auth-forgot-reset-form > h2,
#auth-federated-consent-dialog[open] #auth-federated-consent-form > h2,
#auth-delete-confirm-dialog[open] #auth-delete-confirm-form > h2,
#auth-mfa-setup-dialog[open] #auth-mfa-setup-form > h2,
#auth-mfa-disable-dialog[open] #auth-mfa-disable-form > h2,
#auth-mfa-challenge-dialog[open] #auth-mfa-challenge-form > h2 {
  position: sticky;
  top: 0;
  z-index: 10;
  flex: 0 0 auto;
  margin: 0;
  padding: calc(env(safe-area-inset-top) + 8px) 56px 12px;
  background: linear-gradient(180deg, var(--glass-strong) 75%, transparent);
  -webkit-backdrop-filter: blur(14px);
  backdrop-filter: blur(14px);
  min-height: 56px;
  display: flex;
  align-items: center;
  justify-content: center;
  text-align: center;
  /* Match the canonical dialog title (1.55rem italic 700) — same size the
     report / frost-modal headers were already bumped to. The old --fs-lg
     (18px) read too small for a mobile auth title (Petřin pokyn 2026-06-01). */
  font: italic 700 1.55rem var(--font-display);
  color: var(--text-dark);
  letter-spacing: 0.025em;
}

/* ── Back arrow — reskin the auto-injected .dialog-close (modalUtils
 * injects it as dialog.firstChild for every dialog without
 * data-no-auto-close-button). It already calls dialog.close() = "go
 * back", so the semantics are correct with zero JS. Pull it from the
 * absolute top-right corner into the top-left gutter and swap × → ←. */
#auth-verify-dialog[open] > .dialog-close,
#auth-forgot-email-dialog[open] > .dialog-close,
#auth-forgot-reset-dialog[open] > .dialog-close,
#auth-federated-consent-dialog[open] > .dialog-close,
#auth-delete-confirm-dialog[open] > .dialog-close,
#auth-mfa-setup-dialog[open] > .dialog-close,
#auth-mfa-disable-dialog[open] > .dialog-close,
#auth-mfa-challenge-dialog[open] > .dialog-close {
  position: absolute;
  top: calc(env(safe-area-inset-top) + 12px);
  left: 12px;
  right: auto;
  width: 40px;
  height: 40px;
  min-width: 40px;
  font-size: 0;              /* hide the × text node */
  z-index: 11;
  /* Plain teal chevron, NO disc (Petřin pokyn 2026-06-01 "žádná vyplněná
     elipsa kolem") — override the base .dialog-close frosted disc so the
     auth back arrow matches the canonical .fm-sheet__back. Higher specificity
     than the base hover/focus rules, so the disc never re-appears. */
  background: transparent;
  border: 0;
  box-shadow: none;
  -webkit-backdrop-filter: none;
  backdrop-filter: none;
  color: var(--accent-turquoise);
}
#auth-verify-dialog[open] > .dialog-close::before,
#auth-forgot-email-dialog[open] > .dialog-close::before,
#auth-forgot-reset-dialog[open] > .dialog-close::before,
#auth-federated-consent-dialog[open] > .dialog-close::before,
#auth-delete-confirm-dialog[open] > .dialog-close::before,
#auth-mfa-setup-dialog[open] > .dialog-close::before,
#auth-mfa-disable-dialog[open] > .dialog-close::before,
#auth-mfa-challenge-dialog[open] > .dialog-close::before {
  /* Same chevron glyph as the canonical .fm-sheet__back (SVG path masked in
     the button's teal colour) — one back-arrow shape app-wide. */
  content: "";
  display: block;
  width: 22px;
  height: 22px;
  background: currentColor;
  -webkit-mask: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="black" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M15 6l-6 6 6 6"/></svg>') center / 22px no-repeat;
  mask: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="black" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M15 6l-6 6 6 6"/></svg>') center / 22px no-repeat;
}

/* ── Scrolling body — Family A: the <form> scrolls between sticky
 * head + foot; give the field flow comfortable side gutters. The
 * h2 (sticky) and .form-actions (sticky) are excluded. */
#auth-login-dialog[open] > #auth-login-form,
#auth-register-dialog[open] > #auth-register-form,
#auth-verify-dialog[open] > #auth-verify-form,
#auth-forgot-email-dialog[open] > #auth-forgot-email-form,
#auth-forgot-reset-dialog[open] > #auth-forgot-reset-form,
#auth-federated-consent-dialog[open] > #auth-federated-consent-form,
#auth-delete-confirm-dialog[open] > #auth-delete-confirm-form,
#auth-mfa-setup-dialog[open] > #auth-mfa-setup-form,
#auth-mfa-disable-dialog[open] > #auth-mfa-disable-form,
#auth-mfa-challenge-dialog[open] > #auth-mfa-challenge-form {
  overflow-y: auto;
  -webkit-overflow-scrolling: touch;
}
#auth-login-dialog[open] > #auth-login-form > :not(h2):not(.form-actions):not(.event-create__header),
#auth-register-dialog[open] > #auth-register-form > :not(h2):not(.form-actions):not(.event-create__header),
#auth-verify-dialog[open] > #auth-verify-form > :not(h2):not(.form-actions),
#auth-forgot-email-dialog[open] > #auth-forgot-email-form > :not(h2):not(.form-actions),
#auth-forgot-reset-dialog[open] > #auth-forgot-reset-form > :not(h2):not(.form-actions),
#auth-federated-consent-dialog[open] > #auth-federated-consent-form > :not(h2):not(.form-actions),
#auth-delete-confirm-dialog[open] > #auth-delete-confirm-form > :not(h2):not(.form-actions),
#auth-mfa-setup-dialog[open] > #auth-mfa-setup-form > :not(h2):not(.form-actions),
#auth-mfa-disable-dialog[open] > #auth-mfa-disable-form > :not(h2):not(.form-actions),
#auth-mfa-challenge-dialog[open] > #auth-mfa-challenge-form > :not(h2):not(.form-actions) {
  margin-left: 14px;
  margin-right: 14px;
}
/* Google button crop fix (Petřin pokyn 2026-06-01): the gutter margins
   above shrink normal block children, but .auth-google-btn carries an
   explicit width:100% from style.css, so 100% + 28px margins overflowed
   the form and clipped its right edge. Drop the fixed width so it fills
   the gutter-bounded width like the fields. */
#auth-login-dialog[open] > #auth-login-form > .auth-google-btn,
#auth-login-dialog[open] > #auth-login-form > .auth-magic-btn,
#auth-register-dialog[open] > #auth-register-form > .auth-google-btn {
  width: auto;
}
/* First scrolling element clears the sticky header with breathing room. */
#auth-login-dialog[open] > #auth-login-form > :not(h2):not(.form-actions):not(.event-create__header):first-of-type,
#auth-register-dialog[open] > #auth-register-form > :not(h2):not(.form-actions):not(.event-create__header):first-of-type,
#auth-verify-dialog[open] > #auth-verify-form > :not(h2):not(.form-actions):first-of-type,
#auth-forgot-email-dialog[open] > #auth-forgot-email-form > :not(h2):not(.form-actions):first-of-type,
#auth-forgot-reset-dialog[open] > #auth-forgot-reset-form > :not(h2):not(.form-actions):first-of-type,
#auth-federated-consent-dialog[open] > #auth-federated-consent-form > :not(h2):not(.form-actions):first-of-type,
#auth-delete-confirm-dialog[open] > #auth-delete-confirm-form > :not(h2):not(.form-actions):first-of-type,
#auth-mfa-setup-dialog[open] > #auth-mfa-setup-form > :not(h2):not(.form-actions):first-of-type,
#auth-mfa-disable-dialog[open] > #auth-mfa-disable-form > :not(h2):not(.form-actions):first-of-type,
#auth-mfa-challenge-dialog[open] > #auth-mfa-challenge-form > :not(h2):not(.form-actions):first-of-type {
  margin-top: 10px;
}

/* ── Family B headers (Profile / Security / Preferences / Your data) —
 * these dialogs carry the desktop .modal-band <header> (close + h2
 * wrapped; Petřin pokyn 2026-06-04), so the bare-h2 / direct
 * .dialog-close rules above never match them. Give the header the SAME
 * canonical glass sticky bar as the event-create forms (ui-standards §2
 * mobile rule a: ← teal chevron · centred title · 40px spacer; the band
 * wash itself is desktop-only and never renders here). */
#auth-login-dialog[open] .event-create__header,
#auth-register-dialog[open] .event-create__header,
#auth-profile-dialog[open] .event-create__header,
#auth-preferences-dialog[open] .event-create__header,
#auth-security-dialog[open] .event-create__header,
#auth-data-dialog[open] .event-create__header {
  position: sticky;
  top: 0;
  z-index: 10;
  flex: 0 0 auto;
  margin: 0;
  padding: calc(env(safe-area-inset-top) + 8px) 14px 12px;
  background: linear-gradient(180deg, var(--glass-strong) 75%, transparent);
  -webkit-backdrop-filter: blur(14px);
  backdrop-filter: blur(14px);
  display: flex;
  align-items: center;
}
#auth-login-dialog[open] .event-create__header h2,
#auth-register-dialog[open] .event-create__header h2,
#auth-profile-dialog[open] .event-create__header h2,
#auth-preferences-dialog[open] .event-create__header h2,
#auth-security-dialog[open] .event-create__header h2,
#auth-data-dialog[open] .event-create__header h2 {
  flex: 1;
  order: 2;            /* between back arrow (order 1) + spacer (order 3) */
  margin: 0;
  padding: 0 4px;
  text-align: center;
  font: italic 700 1.55rem var(--font-display);
  color: var(--text-dark);
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
/* Right-side spacer keeps the title optically centred opposite the 40px
 * back arrow — same ::after trick as the event-create headers. */
#auth-login-dialog[open] .event-create__header::after,
#auth-register-dialog[open] .event-create__header::after,
#auth-profile-dialog[open] .event-create__header::after,
#auth-preferences-dialog[open] .event-create__header::after,
#auth-security-dialog[open] .event-create__header::after,
#auth-data-dialog[open] .event-create__header::after {
  content: "";
  order: 3;
  width: 40px;
  flex: 0 0 auto;
}
/* Back arrow — reskin the in-header .dialog-close: × → ← teal chevron
 * (canonical .fm-sheet__back glyph), pulled into the header flow. */
#auth-login-dialog[open] .event-create__header .dialog-close,
#auth-register-dialog[open] .event-create__header .dialog-close,
#auth-profile-dialog[open] .event-create__header .dialog-close,
#auth-preferences-dialog[open] .event-create__header .dialog-close,
#auth-security-dialog[open] .event-create__header .dialog-close,
#auth-data-dialog[open] .event-create__header .dialog-close {
  position: relative;
  order: 1;
  top: auto;
  right: auto;
  left: auto;
  width: 40px;
  height: 40px;
  min-width: 40px;
  font-size: 0;              /* hide the original × text node */
  flex: 0 0 auto;
  z-index: 11;
  background: transparent;
  border: 0;
  box-shadow: none;
  -webkit-backdrop-filter: none;
  backdrop-filter: none;
  color: var(--accent-turquoise);
  transition: color 160ms ease, transform 120ms ease;
}
#auth-login-dialog[open] .event-create__header .dialog-close:active,
#auth-register-dialog[open] .event-create__header .dialog-close:active,
#auth-profile-dialog[open] .event-create__header .dialog-close:active,
#auth-preferences-dialog[open] .event-create__header .dialog-close:active,
#auth-security-dialog[open] .event-create__header .dialog-close:active,
#auth-data-dialog[open] .event-create__header .dialog-close:active {
  color: var(--accent-turquoise-bright);
  transform: scale(0.94);
}
#auth-login-dialog[open] .event-create__header .dialog-close > *,
#auth-register-dialog[open] .event-create__header .dialog-close > *,
#auth-profile-dialog[open] .event-create__header .dialog-close > *,
#auth-preferences-dialog[open] .event-create__header .dialog-close > *,
#auth-security-dialog[open] .event-create__header .dialog-close > *,
#auth-data-dialog[open] .event-create__header .dialog-close > * {
  display: none;
}
#auth-login-dialog[open] .event-create__header .dialog-close::before,
#auth-register-dialog[open] .event-create__header .dialog-close::before,
#auth-profile-dialog[open] .event-create__header .dialog-close::before,
#auth-preferences-dialog[open] .event-create__header .dialog-close::before,
#auth-security-dialog[open] .event-create__header .dialog-close::before,
#auth-data-dialog[open] .event-create__header .dialog-close::before {
  /* Same chevron glyph as the canonical .fm-sheet__back. */
  content: "";
  display: block;
  width: 22px;
  height: 22px;
  background: currentColor;
  -webkit-mask: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="black" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M15 6l-6 6 6 6"/></svg>') center / 22px no-repeat;
  mask: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="black" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M15 6l-6 6 6 6"/></svg>') center / 22px no-repeat;
}

/* ── Scrolling body — Family B (Profile / Security / Data): the dialog
 * is the column; the middle child (form or .profile-section) grows +
 * scrolls between the sticky h2 and the sticky footer. */
#auth-profile-dialog[open] > #auth-profile-form,
#auth-preferences-dialog[open] > #auth-profile-preferences,
#auth-security-dialog[open] > #auth-profile-security,
#auth-data-dialog[open] > #auth-profile-data {
  flex: 1 1 auto;
  min-height: 0;
  overflow-y: auto;
  -webkit-overflow-scrolling: touch;
  margin: 0;
  padding: 10px 14px 8px;
  border-bottom: 0;             /* drop the desktop section hairline */
}

/* ── Sticky Save bar — the LAST .form-actions in the dialog becomes a
 * full-width sticky footer. Family A: it's inside the form. Family B:
 * Profile's Save lives inside the form's .form-actions; Security/Data
 * use .form-actions--close as their exit row. All get the same bar. */
#auth-login-dialog[open] .form-actions,
#auth-register-dialog[open] .form-actions,
#auth-verify-dialog[open] .form-actions,
#auth-forgot-email-dialog[open] .form-actions,
#auth-forgot-reset-dialog[open] .form-actions,
#auth-federated-consent-dialog[open] .form-actions,
#auth-delete-confirm-dialog[open] .form-actions,
#auth-mfa-setup-dialog[open] .form-actions,
#auth-mfa-disable-dialog[open] .form-actions,
#auth-mfa-challenge-dialog[open] .form-actions,
#auth-profile-dialog[open] #auth-profile-form > .form-actions,
#auth-preferences-dialog[open] #auth-profile-preferences > .form-actions,
#auth-security-dialog[open] > .form-actions--close,
#auth-data-dialog[open] > .form-actions--close {
  position: sticky;
  bottom: 0;
  z-index: 10;
  flex: 0 0 auto;
  margin: 0;
  padding: 12px 14px calc(env(safe-area-inset-bottom) + 18px);
  background: linear-gradient(180deg, transparent 0%, var(--glass-strong) 30%);
  -webkit-backdrop-filter: blur(14px);
  backdrop-filter: blur(14px);
  display: flex;
  flex-direction: column-reverse;   /* primary on top, ghost below */
  gap: 10px;
}

/* Settings sheets (Petřin pokyn 2026-06-04): the glass-strong tint read
 * as a blue strip behind the Save/Close button — these sheets mostly fit
 * the screen, so the footer sits mid-gradient and ANY tint shows as a
 * band edge. Fully transparent here; the button itself is opaque. */
#auth-profile-dialog[open] #auth-profile-form > .form-actions,
#auth-preferences-dialog[open] #auth-profile-preferences > .form-actions,
#auth-security-dialog[open] > .form-actions--close,
#auth-data-dialog[open] > .form-actions--close {
  background: none;
  -webkit-backdrop-filter: none;
  backdrop-filter: none;
}

/* The back arrow (reskinned .dialog-close) already provides "cancel /
 * go back", so the redundant ghost Cancel in the sticky bar is hidden —
 * matching the MO-5 fullscreen forms + mockups 25/26 (no ghost cancel).
 * We target ONLY the dismissal buttons (data-close-dialog /
 * federated-consent-cancel); distinct actions like "Resend code"
 * (.btn-link) and the primary submit stay. The two Family-B dialogs
 * (Security / Data) keep their explicit "Close" as the sole footer
 * action — they have no submit, so the footer would otherwise be empty. */
#auth-login-dialog[open] .form-actions > button[data-close-dialog],
#auth-register-dialog[open] .form-actions > button[data-close-dialog],
#auth-verify-dialog[open] .form-actions > button[data-close-dialog],
#auth-forgot-email-dialog[open] .form-actions > button[data-close-dialog],
#auth-forgot-reset-dialog[open] .form-actions > button[data-close-dialog],
#auth-federated-consent-dialog[open] .form-actions > #auth-federated-consent-cancel,
#auth-delete-confirm-dialog[open] .form-actions > button[data-close-dialog],
#auth-mfa-setup-dialog[open] .form-actions > button[data-close-dialog],
#auth-mfa-disable-dialog[open] .form-actions > button[data-close-dialog],
#auth-mfa-challenge-dialog[open] .form-actions > button[data-close-dialog],
#auth-profile-dialog[open] #auth-profile-form > .form-actions > button[data-close-dialog],
#auth-preferences-dialog[open] #auth-profile-preferences > .form-actions > button[data-close-dialog] {
  display: none;
}

/* Security/Data: the action buttons (Enable/Disable 2FA · Export ·
 * Delete) live in a .form-actions INSIDE .profile-section — give that
 * inner row a stacked full-width layout in the scrolling body (it is
 * NOT the sticky footer; the footer is .form-actions--close). */
#auth-security-dialog[open] #auth-profile-security > .form-actions,
#auth-data-dialog[open] #auth-profile-data > .form-actions {
  display: flex;
  flex-direction: column;
  gap: 10px;
  margin: 14px 0 0;
}

/* ── Full-width buttons in the sticky bars + Security/Data action rows.
 * Resize + repaint only — every button keeps its id / class / handler.
 * Submit buttons keep their copper (auth) / teal (.btn-primary) /
 * danger gradients from style.css; we just make them full-width. */
#auth-login-dialog[open] .form-actions > button,
#auth-register-dialog[open] .form-actions > button,
#auth-verify-dialog[open] .form-actions > button,
#auth-forgot-email-dialog[open] .form-actions > button,
#auth-forgot-reset-dialog[open] .form-actions > button,
#auth-federated-consent-dialog[open] .form-actions > button,
#auth-delete-confirm-dialog[open] .form-actions > button,
#auth-mfa-setup-dialog[open] .form-actions > button,
#auth-mfa-disable-dialog[open] .form-actions > button,
#auth-mfa-challenge-dialog[open] .form-actions > button,
#auth-profile-dialog[open] #auth-profile-form > .form-actions > button,
#auth-preferences-dialog[open] #auth-profile-preferences > .form-actions > button,
#auth-security-dialog[open] .form-actions > button,
#auth-data-dialog[open] .form-actions > button {
  width: 100%;
  margin: 0;
}
/* Full-width primary (submit / settings Save / Enable 2FA) gets the
 * taller fullscreen rhythm + pill radius matching MO-5 Save bars. */
#auth-login-dialog[open] .form-actions > button[type="submit"],
#auth-register-dialog[open] .form-actions > button[type="submit"],
#auth-verify-dialog[open] .form-actions > button[type="submit"],
#auth-forgot-email-dialog[open] .form-actions > button[type="submit"],
#auth-forgot-reset-dialog[open] .form-actions > button[type="submit"],
#auth-federated-consent-dialog[open] .form-actions > button[type="submit"],
#auth-delete-confirm-dialog[open] .form-actions > button[type="submit"],
#auth-mfa-setup-dialog[open] .form-actions > button[type="submit"],
#auth-mfa-disable-dialog[open] .form-actions > button[type="submit"],
#auth-mfa-challenge-dialog[open] .form-actions > button[type="submit"],
#auth-profile-dialog[open] #auth-profile-form > .form-actions > button[type="submit"],
#auth-preferences-dialog[open] #auth-profile-preferences > .form-actions > button[type="submit"],
#auth-security-dialog[open] #auth-profile-security #auth-mfa-enable,
#auth-security-dialog[open] #auth-profile-security #auth-mfa-disable,
#auth-data-dialog[open] #auth-profile-data #auth-data-export,
#auth-data-dialog[open] #auth-profile-data #auth-data-delete {
  padding: 15px;
  border-radius: 14px;
  font: 700 var(--fs-md, 14px) var(--font-body);
  letter-spacing: 0.04em;
}
/* .form-actions--span buttons carry flex: 1 1 0 from style.css for the
 * DESKTOP row geometry; in the mobile column-reverse sticky bar that
 * basis-0 flex acts VERTICALLY (squash/stretch with the viewport) and
 * pushed the Sign in label off the button's vertical centre. Natural
 * height + centred inline-flex label instead. */
#auth-login-dialog[open] .form-actions--span > button,
#auth-register-dialog[open] .form-actions--span > button,
#auth-profile-dialog[open] .form-actions--span > button,
#auth-preferences-dialog[open] .form-actions--span > button {
  flex: 0 0 auto;
}
#auth-login-dialog[open] .form-actions--span > button[type="submit"],
#auth-register-dialog[open] .form-actions--span > button[type="submit"],
#auth-profile-dialog[open] .form-actions--span > button[type="submit"],
#auth-preferences-dialog[open] .form-actions--span > button[type="submit"] {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  line-height: 1;
}

/* ── Field rhythm — comfortable vertical spacing so the form breathes
 * at fullscreen (mockup cadence). The gap BETWEEN field groups must clearly
 * exceed the gap WITHIN a group (label→input→hint = 0.4rem ≈ 6px) so a label
 * reads as the start of its own field, not a continuation of the previous
 * field's hint (Gestalt proximity). 16px was too tight — the hint of one field
 * touched the next field's label; 32px then over-corrected ("přehnal"). Settled
 * on --space-6 = 24px, the middle (Petřin pokyn 2026-05-31). */
#auth-login-dialog[open] .field,
#auth-register-dialog[open] .field,
#auth-verify-dialog[open] .field,
#auth-forgot-email-dialog[open] .field,
#auth-forgot-reset-dialog[open] .field,
#auth-federated-consent-dialog[open] .field,
#auth-delete-confirm-dialog[open] .field,
#auth-mfa-setup-dialog[open] .field,
#auth-mfa-challenge-dialog[open] .field,
#auth-profile-dialog[open] #auth-profile-form > .field,
#auth-preferences-dialog[open] #auth-profile-preferences > .field {
  margin-bottom: var(--space-6);
}

/* Tighter label→input→hint inside auth fields (Petřin pokyn 2026-05-31).
   Non-checkbox only so the checkbox box→label gap stays. */
#auth-login-dialog[open] .field:not(.field--checkbox),
#auth-register-dialog[open] .field:not(.field--checkbox) {
  gap: var(--space-1);
}

/* Age + consent checkboxes = one logical group → tight, not the full 24px
   inter-field gap (the 44px tap target already adds height, so 24px on top
   read as a huge hole between the two boxes — Petřin pokyn 2026-05-31). */
#auth-login-dialog[open] .field--checkbox,
#auth-register-dialog[open] .field--checkbox {
  margin-bottom: var(--space-2);
  /* Kill the 44px min-height dead space below the single-line "16+" row that
     inflated the gap to the consent box (Petřin pokyn 2026-05-31). Row still
     sizes to content, full-width tap, above WCAG AA 24px. */
  min-height: 0;
  padding: var(--space-1) 0;
}

/* iOS Safari auto-zoom guard (P-033): keep typed inputs ≥ 16px so the
 * browser doesn't zoom on focus. The 6-digit code inputs already use a
 * larger size from style.css; this floors the rest. */
#auth-login-dialog[open] input,
#auth-register-dialog[open] input,
#auth-verify-dialog[open] input,
#auth-forgot-email-dialog[open] input,
#auth-forgot-reset-dialog[open] input,
#auth-federated-consent-dialog[open] input,
#auth-delete-confirm-dialog[open] input[type="text"],
#auth-mfa-setup-dialog[open] input,
#auth-mfa-challenge-dialog[open] input,
#auth-profile-dialog[open] input,
#auth-preferences-dialog[open] input,
#auth-preferences-dialog[open] select {
  font-size: 16px;
}

/* The default-location search keeps room for its inline magnifier
   (.input-affix, account-modals redesign 2026-06-04) — the generic mobile
   input padding above would otherwise slide the text under the glyph. */
#auth-preferences-dialog[open] .input-affix > input {
  padding-left: 2.2rem;
}

/* ── Sign-in links (Forgot password? / Sign up) keep the canonical teal
 * .btn-link on mobile too — the copper override from mobile mockup 25 made
 * the same link gold on phone and teal on desktop (Petřin pokyn 2026-06-04:
 * "mělo by být modré i na mobilu"). One link colour across devices. */

/* ── Helper-text voice (mockups 25/26): the nickname / password rule
 * captions read as soft italic flavour text, matching the mobile auth
 * artboards (and the .field__hint--soft / radio-sub voice used elsewhere
 * in Noir). Mobile-only; the desktop dialog keeps the upright hint. */
#auth-register-dialog[open] .field__hint,
#auth-register-dialog[open] .form-info,
#auth-login-dialog[open] .field__hint {
  font-style: italic;
}

/* =============================================================
 * MO-7 — SYSTEM & ONBOARDING surfaces (mobile treatment)
 *
 * Phone-width (≤480px) reskin for the app's system / onboarding
 * surfaces so they read as part of the shipped Noir mobile product:
 * cookie banner · maintenance overlay · banned banner · LIVE banner ·
 * songs placeholder · generic Frost confirm / rejection dialogs ·
 * empty / loading / error states · + the new image lightbox.
 *
 * Surface-only: no logic / data / endpoint change. Status colours are
 * sacred and left untouched (banned/danger = brick; live = its green
 * accent). Derived from m12-primitives.md (banners/overlays) + the
 * MO-1…MO-6 .fm-* battery + tokens. Banners respect safe-area, sit at
 * the right --m13-z-* layer, keep ≥44px targets, honour reduced-motion.
 * ============================================================= */

/* ── Cookie banner — full-width bottom card above the tab bar ──
 * Desktop is a 460px floating pill bottom-centre; on phone it spans the
 * width and clears the bottom tab bar + the iOS gesture inset so it never
 * sits under the navigation. Copper "Got it" CTA is unchanged (system tone). */
.cookie-banner {
  left: 8px;
  right: 8px;
  transform: translateY(120%);
  width: auto;
  max-width: none;
  margin-bottom: calc(var(--m13-tab-bar-h) + env(safe-area-inset-bottom) + 10px);
  z-index: var(--m13-z-quick-menu);
}
.cookie-banner.cookie-banner--visible {
  transform: translateY(0);
}
.cookie-banner__btn {
  align-self: stretch;
  text-align: center;
}

/* ── Maintenance overlay — fullscreen, panel fills the width ──
 * Already inset:0 fixed; on phone the panel goes near-full-width with
 * comfortable padding + safe-area, and the title scales down to the
 * mobile display size. Sits above everything (its own z 2000). */
.maintenance-overlay {
  padding: 16px;
  padding-top: max(16px, env(safe-area-inset-top));
  padding-bottom: max(16px, env(safe-area-inset-bottom));
}
.maintenance-overlay__panel {
  max-width: 100%;
  padding: 2rem 1.4rem;
  border-radius: var(--m13-sheet-radius);
}
.maintenance-overlay__title {
  font-size: var(--fs-xl, 1.3rem);
}

/* ── Banned banner — full-width brick alert, clears the header pill ──
 * Stays the sacred brick danger family; on phone it tucks below the
 * top safe-area so it isn't hidden behind the notch, and reads as a
 * full-width status bar. */
.banned-banner {
  position: relative;
  z-index: var(--m13-z-chrome);
  padding: calc(env(safe-area-inset-top) + 0.7rem) 1rem 0.7rem;
  font-size: 0.88rem;
  line-height: 1.4;
}

/* (Removed 2026-06-06, Petřin pokyn: mobile .live-banner overrides — the
   persistent LIVE banner is gone app-wide; satellite pill + tab dot.) */

/* ── Songs placeholder dialog — fullscreen sheet on phone ──
 * The "coming soon" / leaderboard dialog becomes a fullscreen step like the
 * MO-5/6 forms so the ranked list has room. Reuses the fullscreen-dialog
 * shell treatment (sticky title, scrolling body) via its own rules — the
 * generic dialog-fullscreen block below covers the chrome. */
/* Journey shares the chart's fullscreen-sheet treatment on phone (Petřin pokyn
   2026-06-02 — the rating/journey is a desktop modal but a full sheet like the
   chart on mobile). */
#songs-dialog[open],
#journey-dialog[open] {
  position: fixed;
  inset: 0;
  margin: 0;
  width: 100vw;
  max-width: 100vw;
  height: 100dvh;
  max-height: 100dvh;
  border: 0;
  border-radius: 0;
  padding: calc(env(safe-area-inset-top) + 14px) 16px calc(env(safe-area-inset-bottom) + 14px);
  overflow-y: auto;
  -webkit-overflow-scrolling: touch;
  background: radial-gradient(ellipse 80% 40% at 50% 0%, rgba(var(--accent-violet-rgb), 0.12) 0%, transparent 60%),
              linear-gradient(180deg, var(--cream-surface) 0%, var(--cream-base) 100%);
}
#songs-dialog[open]::backdrop,
#journey-dialog[open]::backdrop {
  background: var(--cream-base);
  -webkit-backdrop-filter: none;
  backdrop-filter: none;
}
/* Remaining-stars meter stays pinned while the tile grid scrolls (Petřin pokyn
   2026-06-02). The dialog is the scroll container. A SOLID fill stops the tiles
   bleeding through underneath. The title is NOT sticky — a sticky bar over the
   dialog's gradient drew an ugly dark stripe + covered the × (Petřin pokyn). */
#journey-dialog[open] .journey__meter {
  position: sticky;
  top: 0;
  z-index: 2;
  background: var(--cream-surface);
}
/* ── Unified mobile header (header audit, 2026-06-03) — the top-right ×
 * becomes the canonical top-LEFT teal back chevron (same masked glyph as
 * .fm-sheet__back and the create/auth dialogs) and the head row centres,
 * so Chart + Journey match the app-wide fullscreen pattern: ← back ·
 * centred title. position:absolute (same as the old ×) — it scrolls away
 * with the head, so the journey's pinned STAR BUDGET meter never collides
 * with it (the meter is sticky top:0 and the dialog is the scroll
 * container). The chart-mobile.png handoff header (× top-right) is
 * superseded by this. */
#songs-dialog[open] > .dialog-close,
#journey-dialog[open] > .dialog-close {
  position: absolute;
  top: calc(env(safe-area-inset-top) + 10px);
  left: 12px;
  right: auto;
  width: 40px;
  height: 40px;
  z-index: 12;
  font-size: 0;           /* hide the × text node (chart close) */
  /* Plain teal chevron, NO disc — match the canonical .fm-sheet__back. */
  background: transparent;
  border: 0;
  box-shadow: none;
  -webkit-backdrop-filter: none;
  backdrop-filter: none;
  color: var(--accent-turquoise);
  transition: color 160ms ease, transform 120ms ease;
}
#songs-dialog[open] > .dialog-close:active,
#journey-dialog[open] > .dialog-close:active {
  color: var(--accent-turquoise-bright);
  transform: scale(0.94);
}
/* Focus = brighten, no ring — mirrors .fm-sheet__back:focus-visible. Without
 * this the journey close (no app focus style of its own) shows the UA auto
 * outline around the bare chevron when showModal() autofocuses it on open. */
#songs-dialog[open] > .dialog-close:focus-visible,
#journey-dialog[open] > .dialog-close:focus-visible {
  color: var(--accent-turquoise-bright);
  outline: none;
}
/* Hide the original glyph children (× span / #icon-close svg) + paint the
 * shared chevron via ::before — same data-URI mask as the create dialogs. */
#songs-dialog[open] > .dialog-close > *,
#journey-dialog[open] > .dialog-close > * {
  display: none;
}
#songs-dialog[open] > .dialog-close::before,
#journey-dialog[open] > .dialog-close::before {
  content: "";
  display: block;
  width: 22px;
  height: 22px;
  background: currentColor;
  -webkit-mask: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="black" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M15 6l-6 6 6 6"/></svg>') center / 22px no-repeat;
  mask: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="black" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M15 6l-6 6 6 6"/></svg>') center / 22px no-repeat;
}
/* Centre the head rows between symmetric 44px gutters (the left one clears
 * the back arrow). Title only — the head icons were dropped entirely
 * (Petřin pokyn 2026-06-03, header audit). */
#songs-dialog[open] .songs-dialog__head,
#journey-dialog[open] .journey__head {
  justify-content: center;
  padding: 0 44px;
  text-align: center;
}
/* Intro line reads as the subtitle under the centred title. (Descendant, not
 * direct child — desktop wraps head+intro in .songs-dialog__header since
 * M13.X-HEADER-UNIFY; same mobile render either way.) */
#songs-dialog[open] .songs-dialog__intro {
  text-align: center;
  padding: 0 24px;
}

/* ── Generic Frost confirm / prompt / rejection dialogs ──
 * These stay CENTRED cards (a confirm is a quick decision — fullscreen would
 * be heavy-handed), but on a narrow phone we widen them to the available
 * width, floor the action buttons at 44px, and let them stack full-width so
 * the destructive/confirm choice is an easy thumb target. Backdrop-to-close
 * stays OFF (modalUtils close policy) — × / button / Esc only. */
.confirm-dialog {
  max-width: calc(100vw - 24px);
  width: calc(100vw - 24px);
}
.confirm-dialog .form-actions,
#event-rejection-reason-dialog[open] .form-actions,
#admin-confirm-dialog[open] .form-actions,
#admin-mod-reject-dialog[open] .form-actions {
  flex-direction: column-reverse;
  align-items: stretch;
  gap: 0.5rem;
}
.confirm-dialog .form-actions > button,
#event-rejection-reason-dialog[open] .form-actions > button,
#admin-confirm-dialog[open] .form-actions > button,
#admin-mod-reject-dialog[open] .form-actions > button {
  width: 100%;
  min-height: 44px;
  justify-content: center;
}
/* Frost-modal rejection / confirm cards (centred) — same width clamp. */
#event-rejection-reason-dialog.frost-modal,
#admin-confirm-dialog.frost-modal,
#admin-mod-reject-dialog.frost-modal {
  max-width: calc(100vw - 24px);
  width: calc(100vw - 24px);
}

/* ── Loading / skeleton / empty / error states ──
 * The map skeleton (inline <head> CSS) already fills the viewport; nothing
 * to add. The in-panel empty/loading/error blocks get a touch more breathing
 * room on phone so they don't read as cramped inside the fullscreen tab list.
 * Tokens only — no new colours. */
.feed-empty,
.events-empty-composite {
  margin: 1rem 0.75rem;
}
.feed-mine-empty {
  margin: 0.6rem 0.75rem 0.2rem;
}
.fm-detail__loading,
.fm-detail__error {
  padding: 24px 12px;
}

/* ── 20. .fm-lightbox — GDPR-safe fullscreen photo zoom (MO-7) ──
 * A dependency-free image zoomer (imageLightbox.js). Sits ABOVE every other
 * mobile surface (z = quick-menu + 2) because a tapped photo is the most
 * transient top layer. Deep-ink backdrop; the image is centred in a
 * pinch-zoom stage (native gesture, no JS). Tap the backdrop or the × to
 * dismiss (tap-to-dismiss is the expected lightbox pattern — the one place
 * backdrop-close is intentional). No external code = privacy intact. */
.fm-lightbox {
  position: fixed;
  inset: 0;
  z-index: calc(var(--m13-z-quick-menu) + 2);
  display: flex;
  align-items: center;
  justify-content: center;
  padding: max(16px, env(safe-area-inset-top)) 12px
           max(16px, env(safe-area-inset-bottom));
  background: var(--m13-backdrop-color);
  opacity: 0;
  transition: opacity 200ms ease;
}
.fm-lightbox.is-open { opacity: 1; }

.fm-lightbox__stage {
  max-width: 100%;
  max-height: 100%;
  overflow: auto;
  -webkit-overflow-scrolling: touch;
  /* Native pinch-zoom + pan; no JS gesture handling needed. */
  touch-action: pinch-zoom;
  display: flex;
  align-items: center;
  justify-content: center;
}
.fm-lightbox__img {
  max-width: 100%;
  max-height: 100%;
  width: auto;
  height: auto;
  object-fit: contain;
  border-radius: 12px;
  /* Subtle inner-gradient depth doesn't read on a dark backdrop; a hairline
   * keeps the photo edge crisp without a shadow that would vanish. */
  border: 1px solid var(--border-on-light);
}
.fm-lightbox__close {
  position: absolute;
  top: calc(env(safe-area-inset-top) + 10px);
  right: 12px;
  width: 44px;
  height: 44px;
  border-radius: 50%;
  display: grid;
  place-items: center;
  background: var(--glass-strong);
  -webkit-backdrop-filter: blur(12px);
  backdrop-filter: blur(12px);
  border: 1px solid var(--border-strong-light);
  color: var(--text-dark);
  cursor: pointer;
}
@media (prefers-reduced-motion: reduce) {
  .fm-lightbox { transition: none; }
}

/* M13.X-CHROME-REFLOW — admin/backstage mobile card layout stays phone-only
   (≤480px). The kebab action-sheet that REPLACES the inline row icons is
   injected by adminMobile*.js at ≤480px; these card styles hide the inline
   icons, so lifting them to 767px without the JS would leave admin rows with
   no visible actions. Admin is a separate Petra-only overlay → deferred to the
   "polish later" follow-up. Nested inside the 767px block above → the effective
   query for everything in here is (max-width: 480px). */
@media (max-width: 480px) {

/* =============================================================
 * BS-1 — BACKSTAGE (admin) MOBILE FOUNDATION   (M12D-2, ADR-180)
 *
 * The admin/moderation area is a SEPARATE page (admin.html) that
 * renders its data as <table>s in JS (adminEventsUI.js & co.). On a
 * phone those tables overflow horizontally, the action-icon clusters
 * squash, and filter selects become unreachable.
 *
 * JS-ARCHITECTURE DECISION = OPTION A (CSS-only responsive reflow).
 * ─────────────────────────────────────────────────────────────────
 *   Every admin table is semantic markup with stable cell classes
 *   (.admin-row__title / __meta / __when, .admin-actions) and status
 *   sections injected as <tr class="admin-mod-section--{kind}">. That
 *   reflows into a card list purely via CSS `display:block` — NO
 *   renderer JS is touched, so permission gating, moderator anonymity,
 *   and moderation behaviour carry zero risk. The desktop table DOM is
 *   left byte-for-byte intact; only its ≤480px *presentation* changes.
 *   (Option B — a mobile render branch in JS — was rejected: the
 *   moderation render path is sensitive and off-limits for rewrite.)
 *
 * BS-1 SCOPE = foundation only. This block establishes:
 *   • the admin layer namespace (bs-*) ON TOP of the fan fm-* engine
 *     — sheets / forms / action sheet are REUSED from fm-* (no parallel
 *     engine, per ADR-180 override #1);
 *   • the atmospheric base + chrome reskin (app bar, h-scroll tab strip,
 *     pane card) at ≤480px;
 *   • the table→card REFLOW SCAFFOLD that BS-3 (row card) builds on.
 *   The chrome details / kebab action sheet / filter sheet / forms /
 *   bulk-select are BS-2…BS-7 — NOT built here.
 *
 * Tokenised: every colour resolves through a token; raw px mapped to
 * the --fs-* / --space-* scale; z-index reuses the --m13-z-* scale
 * (already rebased above the z1000 map context, so admin chrome that
 * ever coexists with a map stays on top). Guarded ≤480px so desktop
 * admin (≥720px) is completely unchanged. ADR-180 override #2 (no raw
 * px) + #3 (cancelled = treatment, not hue — see .is-cancelled below).
 * ============================================================= */

/* ── BS.1  Atmospheric base + page chrome ─────────────────────────
 * The desktop .admin-body already paints the plum/teal/copper mesh on
 * --cream-base (Noir dark). On mobile we keep that mesh but tighten the
 * page to a single scrolling column and reskin the sticky chrome into
 * the app-bar + h-scroll tab strip the mockups show. */
.admin-body {
  /* Mesh per the admin-mobile mockups: plum top-left + teal-deep
     bottom-right. Reuses the canonical wash tokens (same ones the
     desktop body + panes use) so the admin colourway stays one system. */
  background-image:
    radial-gradient(at 0% 0%,     var(--frost-wash-plum) 0%, transparent 55%),
    radial-gradient(at 100% 100%, var(--frost-wash-teal) 0%, transparent 55%);
}

.admin-main {
  /* Edge-to-edge single column; the desktop 1280px max-width + auto
     margins + 1.25rem pad collapse to a thin gutter on a phone. */
  padding: var(--space-2);
  max-width: 100%;
}

/* App bar — the sticky desktop 3-col grid header becomes a compact
 * 48px-tall bar: ‹ Map (teal, = leaving admin) · Backstage wordmark
 * (iris Cormorant, de-gradiented per m14-overview) · spacer. The bar is
 * TRANSPARENT over the mesh at rest and gains a glass backing only once
 * the content scrolls under it (.admin-header--scrolled, toggled by
 * adminMobileChrome.js). BS-2 finalises the scrolled state + the wordmark
 * glow + the tab strip below; this builds on the BS-1 base. */
.admin-header {
  padding: calc(env(safe-area-inset-top) + var(--space-2)) var(--space-3) var(--space-2);
  gap: var(--space-2);
  /* Vertically centre the 3 grid columns (back link · title · avatar) — the
     grid default is stretch, which top-aligned "← Back to map" inside its
     stretched cell. Petřin pokyn 2026-05-28 — back to map uprostřed kapsle. */
  align-items: center;
  z-index: var(--m13-z-chrome);
  /* At rest the bar floats over the mesh — no glass, no hairline. The
     desktop rule paints a cream-glass background + bottom border; null
     those out here so the resting mobile bar reads as part of the page
     (mockups 01 + 18). The scrolled modifier re-introduces the glass. */
  background: transparent;
  -webkit-backdrop-filter: none;
  backdrop-filter: none;
  border-bottom: var(--space-px) solid transparent;
  transition: background 180ms ease, border-color 180ms ease,
    -webkit-backdrop-filter 180ms ease, backdrop-filter 180ms ease;
}
/* Scrolled state — content has moved under the bar, so it gains the Noir
   glass backing + a hairline to separate it from the list (mockup 18).
   Toggled by adminMobileChrome.js once scrollY > 4px. */
.admin-header--scrolled {
  background: linear-gradient(180deg, var(--glass-strong) 80%, transparent);
  -webkit-backdrop-filter: blur(14px) saturate(150%);
  backdrop-filter: blur(14px) saturate(150%);
  border-bottom-color: var(--hairline-dark-lo);
}
.admin-header__title {
  /* Italic Cormorant, iris, ~22px. NO gradient background-clip here —
     on a small viewport the masked-to-text gradient renders broken
     (m14-overview §app-bar). Solid iris + a soft plum glow gives the
     atmospheric depth the gradient would have (mockup 18). */
  font-size: var(--fs-xl);
  color: var(--accent-violet-soft);
  text-shadow: 0 0 18px rgba(var(--accent-violet-rgb), 0.33);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.admin-header__back {
  /* Teal back-to-map — signals "you're leaving admin" (m14-overview).
     inline-flex + center: the grid cell already centres the link's BOX
     (.admin-header align-items:center), but the "← Back to map" glyph+text
     still rode high inside the link's own padded box (block baseline). Making
     the link a centred flex line-box settles the arrow + label on one axis. */
  display: inline-flex;
  align-items: center;
  font-size: var(--fs-sm);
  padding: var(--space-2) var(--space-3);
}

/* Tab strip — desktop wrapping flex row becomes a single horizontal-
 * scroll lane (no wrap), so all ~10 canonical tabs (REPORTS / AUDIT LOG /
 * TOURS / CONCERTS / ONLINE EVENTS / LANDMARKS / LIMITS / CREW / USERS /
 * TEST DATA) stay reachable by swiping. Labels unchanged (m14-overview
 * §vocabulary). The strip is ALWAYS glass (it reads against the content
 * scrolling behind it) and sticks just under the app bar. The active tab
 * is recentred by adminMobileChrome.js on mount + tab switch (mockup 19). */
/* Audit log = desktop-only (Petřin pokyn 2026-05-28). The JS gate already
   keeps it out of the mobile tab set; this is the belt-and-suspenders for a
   desktop→mobile resize where the gate (run once at init) didn't re-evaluate. */
#admin-tab-audit { display: none; }

/* The lane that actually SCROLLS. Sticky + glass now live on .admin-tabs-wrap
   (injected by adminMobileChrome.js) so the prev/next arrows sit OUTSIDE the
   scroll area and stay put — horizontal position:sticky on a flex child proved
   unreliable (the arrow rendered off-screen), so we bracket the lane with real
   flex siblings instead. */
.admin-tabs {
  /* basis:0 (NOT auto) — with auto the lane claims its full content width in
     the wrapper's flex layout and pushes the next arrow off-screen; basis 0
     lets it grow from nothing to fill the space between the arrows + scroll. */
  flex: 1 1 0;
  min-width: 0;
  flex-wrap: nowrap;
  overflow-x: auto;
  overflow-y: hidden;
  -webkit-overflow-scrolling: touch;
  scrollbar-width: none;            /* hide the scrollbar lane on mobile */
  padding: 0 0 var(--space-px);
  gap: var(--space-4);              /* roomy lane between tab labels (mockup 19) */
  /* Snap so a tab edge tends to land flush (Petřin pokyn 2026-05-28). */
  scroll-snap-type: x proximity;
  scroll-snap-stop: always;
  scroll-padding-inline: var(--space-3);
  /* Fade both lane edges to transparent so a half-scrolled tab DISSOLVES into
     the strip instead of showing as a hard "sliver". The ‹ › arrows sit just
     outside these fades as the paging affordance. */
  -webkit-mask-image: linear-gradient(90deg, transparent 0, black var(--space-4), black calc(100% - var(--space-4)), transparent 100%);
          mask-image: linear-gradient(90deg, transparent 0, black var(--space-4), black calc(100% - var(--space-4)), transparent 100%);
}
.admin-tabs::-webkit-scrollbar { display: none; }

/* Sticky glass wrapper bracketing [ ‹ | lane | › ]. Carries the sticky + glass
   the lane used to own, so the strip still pins under the app bar and reads
   against the content behind it (m14-overview §3). */
.admin-tabs-wrap {
  display: flex;
  align-items: stretch;
  position: sticky;
  top: calc(env(safe-area-inset-top) + var(--m13-header-pill-h));
  z-index: var(--m13-z-chrome);
  margin: 0 calc(-1 * var(--space-2)) var(--space-3);  /* bleed to the edges */
  padding: 0 var(--space-2);
  background: var(--glass-strong);
  -webkit-backdrop-filter: blur(14px) saturate(150%);
  backdrop-filter: blur(14px) saturate(150%);
  border-bottom: var(--space-px) solid var(--hairline-dark-lo);
}

/* Tab-strip ARROWS (injected by adminMobileChrome.js) — real flex siblings of
   the lane, ALWAYS visible (no sticky). A negative margin pulls each over the
   lane's edge and a solid→transparent gradient masks the half-scrolled tab
   beneath, so the strip reads as "≈3 whole tabs + a clear way to page" with no
   sliver. Petřin pokyn 2026-05-28 — "3 a pak šipky, nikdy ne kousek záložky". */
.admin-tabs-arrow {
  flex: 0 0 auto;
  z-index: 2;
  width: var(--space-7);
  align-self: stretch;
  display: inline-flex;
  align-items: center;
  border: 0;
  padding: 0;
  color: var(--text-dark);
  cursor: pointer;
  transition: opacity 160ms ease;
}
.admin-tabs-arrow {
  background: transparent;          /* sits on the wrapper glass */
}
.admin-tabs-arrow--prev { justify-content: flex-start; }
.admin-tabs-arrow--next { justify-content: flex-end; }
.admin-tabs-arrow svg {
  width: var(--space-4);
  height: var(--space-4);
  flex: 0 0 auto;
}
/* At a scroll end the matching arrow has nothing to reveal — fade it out (it
   keeps its box so the lane width stays stable). */
.admin-tabs-arrow[disabled] {
  opacity: 0;
  pointer-events: none;
}
/* At a scroll extreme drop the fade on that side so the first/last tab reads in
   full (no half-faded active tab at rest). Classes toggled in adminMobileChrome. */
.admin-tabs.admin-tabs--at-start {
  -webkit-mask-image: linear-gradient(90deg, black calc(100% - var(--space-4)), transparent 100%);
          mask-image: linear-gradient(90deg, black calc(100% - var(--space-4)), transparent 100%);
}
.admin-tabs.admin-tabs--at-end {
  -webkit-mask-image: linear-gradient(90deg, transparent 0, black var(--space-4));
          mask-image: linear-gradient(90deg, transparent 0, black var(--space-4));
}
.admin-tabs.admin-tabs--at-start.admin-tabs--at-end {
  -webkit-mask-image: none;
          mask-image: none;
}

/* ── M15.X-ADMIN-HEADER (ADR-263) — sticky pane band, phone variant ──
 * Same strip as desktop, pinned under the app bar + tab strip
 * (--admin-band-top measured by adminUI.js). Compact paddings; the
 * actions may wrap to a second row when labels are long ("+ Add a
 * crew member"), the band grows — content still scrolls beneath it. */
.admin-pane__band {
  top: var(--admin-band-top, 96px);
  z-index: calc(var(--m13-z-chrome) - 1); /* under app bar + tab strip */
  gap: var(--space-2);
  padding: var(--space-2) var(--space-3);
  /* Card panes on mobile keep the desktop card paddings (no ≤480 override
     for .admin-pane--card), so the desktop fuse margins still apply.
     Only tighten the inner rhythm here. */
  background: var(--glass-strong);
  -webkit-backdrop-filter: blur(14px) saturate(150%);
  backdrop-filter: blur(14px) saturate(150%);
  border-bottom: var(--space-px) solid var(--hairline-dark-lo);
}
/* Card panes shrink their padding on mobile (BS-1: --space-3) — match the
   band's edge-fuse margins or the strip would bleed past the card. */
.admin-pane--card > .admin-pane__band {
  margin: calc(-1 * var(--space-3)) calc(-1 * var(--space-3)) var(--space-3);
}
.admin-pane__band .admin-pane__title {
  /* Tab title must outrank the section h2s inside the pane (hierarchy:
     the band names the whole tab) — --fs-lg rendered SMALLER than
     .admin-limits h2, so lift to --fs-xl. */
  font-size: var(--fs-xl);
}
/* Count = key queue-size signal on a phone (Petřin pokyn 2026-05-28 —
   "čísla v adminu moc slabá"). Same lift as the BS-5 toolbar count. */
.admin-pane__band-count {
  font-size: var(--fs-sm);
  font-weight: var(--fw-semibold);
  color: var(--text-dark);
  letter-spacing: 0.04em;
}
.admin-pane__band-actions {
  gap: var(--space-2);
}
.admin-pane__band-actions .btn-primary {
  flex: 0 0 auto;
}
.admin-tab {
  flex: 0 0 auto;                   /* don't shrink — each tab keeps its width */
  scroll-snap-align: start;         /* land flush, never a half-cut tab */
  white-space: nowrap;
  /* inline-flex so the pending ::before dot can be ordered AFTER the label
     (a bare ::before renders before the text). align centre keeps the dot
     vertically aligned with the cap-height of the uppercase label. */
  display: inline-flex;
  align-items: center;
  /* Compact vertical rhythm so the underline pins to the strip's bottom;
     the 44px touch floor is added separately via padding-driven hit area
     in the pointer:coarse block below. */
  padding: var(--space-3) 0 var(--space-2);
  font-size: var(--fs-xs);
}
/* Inactive tabs read muted; the active one keeps the desktop turquoise +
   bolder weight. (Desktop base already colours .admin-tab turquoise — on
   the dark strip that's too loud for the inactive rest, so mute them.) */
.admin-tab:not(.admin-tab--active) {
  color: var(--text-muted);
}
/* Active underline — pin the 2px gradient bar to the STRIP's bottom edge
   (the glass hairline) rather than the desktop cream border. Reuses the
   desktop ::after element; we only re-anchor it and trim the inset so it
   hugs the label width in the tight horizontal lane. */
.admin-tab--active::after {
  left: 0;
  right: 0;
  bottom: 0;
  height: var(--space-0-5);
}
/* Pending dot — desktop paints an absolute corner badge in --danger-bright
   (red). In the horizontal strip the dot reads better INLINE after the
   label, and the chrome tone for "queue items" here is magenta
   (--accent-mine), matching mockup 19 + the BS-2 chrome palette. We
   re-anchor the SAME `data-pending`-driven ::before — no new element, no
   JS change (adminTabIndicators.js still owns when it shows). */
.admin-tab[data-pending]::before {
  position: static;
  order: 2;                          /* push the dot AFTER the label text */
  display: block;
  margin-left: var(--space-1-5);
  top: auto;
  right: auto;
  width: var(--space-1-5);
  height: var(--space-1-5);
  background: var(--accent-mine);
  box-shadow: 0 0 6px var(--accent-mine);
}

/* Pane card — drop the desktop 1.25rem×1.5rem inset to a tighter phone
 * gutter; the mesh + border + radius are inherited from the desktop rule. */
.admin-pane--card {
  padding: var(--space-3) var(--space-3) var(--space-4);
}
.admin-pane__title  { font-size: var(--fs-xl); }
.admin-pane__subtitle { font-size: var(--fs-sm); }

/* ── BS-5  Page structure — header · count · description · action bar ──
 * (M12D-2, ADR-180. Mockup 05-concerts / 03-audit-log = page structure;
 *  mockup 15-filter-sheet-status = the filter sheet below.)
 *
 * PLAIN-LANGUAGE: each Backstage tab's top region — the page title, the
 * item count, the one-line description, and the action bar ("+ New X",
 * Refresh, filter pills) — is laid out as a clean stacked column for the
 * phone instead of the desktop's wrapping single row. NOTHING in the DOM
 * or the JS moves; this is presentation only, guarded ≤480px.
 *
 * The header (title + subtitle) is the desktop .admin-pane__header — its
 * mobile sizes were set in BS-1. Here we finish the ACTION BAR: stack the
 * toolbar into a vertical column, make the primary "+ New X" CTA span the
 * full width (the canonical role-toned .btn-primary / ADR-078 violet keep
 * their own colour — we only change layout), and let the count sit on its
 * own line right under the description-flow rather than floating right. */
.admin-toolbar {
  /* Stays a wrapping flex row (as on desktop) but every full-width child
     (count / CTAs) claims its own line via flex-basis:100%, so the action
     bar reads as a clean vertical stack while filter pills still line up
     side-by-side and wrap (mockup 05 §action-bar). */
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-2);
  margin-bottom: var(--space-3);
}
/* .admin-toolbar__count mobile rule removed (M15.X-ADMIN-HEADER, ADR-263)
   — counts moved into the sticky pane band; the "count carries weight"
   lift (Petřin pokyn 2026-05-28) lives on .admin-pane__band-count above. */
/* "+ New X" CTA = the CANONICAL compact .btn-primary, exactly like the
   popup's teal button (Petřin pokyn 2026-05-31). Two changes vs the old
   M12D-3 treatment: (1) tone is now PER-ENTITY (concert/tour teal, online
   azure, landmark gold) via the .btn-primary--online / --gold variants in
   style.css — no more forced iris (supersedes ADR-078 uniform violet for
   this toolbar); (2) dropped the full-width 44px pill. The wide pill made
   the canonical 13px label look tiny — and per HARD RULE 12 the fix for that
   is a SMALLER box, NOT a bigger font (16px on a button = rejected bug). So
   we let it render at its natural compact size, left-aligned in the lane. */
.admin-body .admin-toolbar > .btn-primary {
  flex: 0 0 auto;
  align-self: flex-start;
}
/* Refresh — the mockups (01/04/05) place it as a COMPACT round ghost icon
   button at the end of the filter row, NOT a full-width gold pill (it was
   rendering loud + visually competing with the real "+ New X" CTA). Shrink
   to a 44px circle, blank the "Refresh" text label, and paint a circular-
   arrow glyph via a mask so the affordance reads as "refresh" not a word. */
.admin-toolbar .auth-btn[data-i18n="admin.refresh"],
.admin-pane__band-actions .auth-btn[data-i18n="admin.refresh"] {
  flex: 0 0 auto;
  order: 10;                        /* sit at the END of the wrapping lane */
  width: 44px;
  min-width: 44px;
  height: 44px;
  min-height: 44px;
  padding: 0;
  margin-left: auto;                /* push to the right edge of the lane */
  border-radius: 999px;
  font-size: 0;                     /* hide the "Refresh" word */
  /* inline-FLEX (not grid): the button still holds the 0-size "Refresh" text
     node next to the ::before glyph. In inline-grid those become two stacked
     rows and the glyph rides high; flex centers the glyph on both axes
     regardless. Petřin pokyn 2026-05-28 — "refresh ať je uprostřed". */
  display: inline-flex;
  align-items: center;
  justify-content: center;
  /* Ghost circle — drop the copper .auth-btn fill so it reads as a quiet
     utility icon next to the loud "+ New X" CTA (mockups 01/04/05). */
  color: var(--text-muted);
  background: transparent;
  border: var(--space-px) solid var(--hairline-dark);
  box-shadow: none;
}
.admin-toolbar .auth-btn[data-i18n="admin.refresh"]:active,
.admin-pane__band-actions .auth-btn[data-i18n="admin.refresh"]:active {
  background: rgba(var(--accent-turquoise-rgb), 0.10);
  color: var(--text-dark);
}
.admin-toolbar .auth-btn[data-i18n="admin.refresh"]::before,
.admin-pane__band-actions .auth-btn[data-i18n="admin.refresh"]::before {
  content: "";
  width: var(--space-5);
  height: var(--space-5);
  background: currentColor;
  -webkit-mask: var(--bs-refresh-glyph) center / contain no-repeat;
          mask: var(--bs-refresh-glyph) center / contain no-repeat;
}
.admin-body {
  --bs-refresh-glyph: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M21 12a9 9 0 1 1-2.64-6.36M21 3v6h-6'/%3E%3C/svg%3E");
}
/* Filter pills that sit DIRECTLY in the toolbar (Reports = 1 pill;
   Test Data = 3 pills) flow as a wrapping inline row, matching the Audit
   tab's .audit-filters lane. */
.admin-toolbar > .filter-pill-wrap {
  flex: 0 0 auto;
}
/* M12D-3 — the filter pill's leading icon is an EMOJI (📋/⏱/👤/🎫/🔎) in the
   shared markup; on a phone it reads off-tone + literal (clipboard / clock /
   face) where the mockups (01/05/15) show a single funnel glyph for every
   filter pill. Blank the emoji + paint a funnel via a mask so all filter
   pills share one crisp mark. Mobile-only; desktop keeps the emoji. */
/* Petřin pokyn 2026-05-28 — ONE indicator only. The leading filter glyph
   (funnel) read as a SECOND triangle next to the right chevron ("two
   triangles" report). Drop it on the phone: the right chevron is the sole
   affordance, and the value beside the label carries the meaning. The dead
   --bs-funnel-glyph mask is removed with it. Mobile-only; desktop keeps its
   emoji glyph (it has room and the value sits inline there too). */
.audit-filter-pill .audit-filter-pill__icon {
  display: none;
}

/* ── BS-5  Filter pills + filter sheet ─────────────────────────────
 * Reports / Audit log / Test Data already render their filters as the
 * canonical .audit-filter-pill + a sibling .filter-dropdown popover, wired
 * by adminFilterDropdown.js (the pill toggles `hidden` + the --open class;
 * an outside tap / Escape / the "Done" button all close it). On a phone an
 * anchored popover clips and the radios/checkboxes inside become a cramped
 * floating card.
 *
 * SAFE-WIRING DECISION = CSS-ONLY (same philosophy as BS-4's mirror):
 *   We do NOT reimplement filtering and we add NO new control. We RESTYLE
 *   the EXISTING open .filter-dropdown into a bottom sheet. The pill still
 *   toggles it, the SAME radios/checkboxes inside still drive the filter,
 *   the SAME "Done"/"Reset" footer still applies/clears via the existing
 *   handlers, and the existing document outside-tap/Escape listeners still
 *   close it. Zero JS, zero logic, zero permission change — results match
 *   desktop exactly because it IS the desktop control, only re-skinned.
 *   (Mockup 15 is the VISUAL reference: handle bar, radio/checkbox rows,
 *   Clear/Apply footer. Option labels/values come from the real control +
 *   en.json, never from the PNG.) */

/* Filter pills scroll horizontally as a lane so several pills (Audit log
   has five) stay reachable by swiping; the active value reads turquoise via
   the desktop .audit-filter-pill__value rule (carried over, one token). */
.audit-filters {
  /* Share the lane with the trailing Refresh circle (mockup 03/05 place
     Refresh at the END of the pill row, not on a line of its own). Grow to
     fill the row but allow shrink-below-content (min-width:0) so the pills
     scroll horizontally inside this flex item instead of pushing Refresh to
     a new line. Petřin pokyn 2026-05-28 — fixes the orphaned Refresh row.
     flex-basis:0 (NOT auto) is essential — with auto, the pills' content width
     counts toward line-breaking and wraps Refresh to its own line; with basis 0
     the lane claims zero intrinsic width, Refresh stays on the row, then the
     lane grows to fill the remainder and scrolls its pills. */
  flex: 1 1 0;
  min-width: 0;
  display: flex;
  flex-wrap: nowrap;
  gap: var(--space-2);
  overflow-x: auto;
  overflow-y: hidden;
  -webkit-overflow-scrolling: touch;
  scrollbar-width: none;
  /* left edge bleeds so a half-cut pill hints "more by swiping"; the right
     side keeps a small gap before the Refresh circle. */
  margin-left: calc(-1 * var(--space-1));
  padding: var(--space-px) var(--space-2) var(--space-px) var(--space-1);
}
.audit-filters::-webkit-scrollbar { display: none; }
.audit-filter-pill {
  flex: 0 0 auto;
  min-height: 44px;                /* touch floor — these are the only filter entry points */
}
/* The pill-wrap is position:relative on desktop (anchor for the popover);
   on a phone the dropdown becomes a fixed bottom sheet, so the wrap must
   not constrain it — let it size to the pill only. */
.filter-pill-wrap {
  display: inline-flex;
  /* The wrap (NOT the inner button) is the flex child of the scroll lane.
     Without flex:0 0 auto it keeps the default flex-shrink:1, so on a narrow
     phone the wraps shrink below their content and the pills visually OVERLAP.
     Pin the natural width so the lane scrolls instead. Petřin pokyn 2026-05-28
     — fixes "filtry se překrývají" on real devices. */
  flex: 0 0 auto;
}

/* The open dropdown → bottom sheet. Reuses the fan sheet's z-scale + the
   Noir glass surface tokens so it reads as the same product as fm-sheet,
   without depending on the fm-* engine markup (the dropdown has its own
   DOM). `[hidden]` keeps it removed when closed (desktop rule unchanged). */
.filter-dropdown:not([hidden]) {
  position: fixed;
  left: 0;
  right: 0;
  bottom: 0;
  top: auto;
  width: auto;
  max-width: none;
  min-width: 0;
  max-height: 80vh;
  z-index: var(--m13-z-sheet);
  display: flex;
  flex-direction: column;
  /* Noir sheet surface — same cream-base the desktop dropdown uses, lifted
     to a top-rounded sheet with the canonical sheet radius + a hairline. */
  background: var(--cream-base);
  border: var(--space-px) solid var(--hairline-dark-lo);
  border-bottom: 0;
  border-radius: var(--radius-lg) var(--radius-lg) 0 0;
  box-shadow: 0 -12px 40px rgba(0, 0, 0, 0.45);
  padding: 0 var(--space-4) calc(env(safe-area-inset-bottom) + var(--space-4));
  /* Slide-up entrance (skipped under reduced-motion via the token below). */
  animation: bs5-filter-sheet-up 240ms cubic-bezier(0.22, 1, 0.36, 1);
}
/* Right-align modifier (set by adminFilterDropdown.js when a desktop popover
   would clip) is meaningless for a full-width sheet — null it out so it
   never re-anchors the sheet off-screen. */
.filter-dropdown:not([hidden]).filter-dropdown--align-right {
  left: 0;
  right: 0;
}
@keyframes bs5-filter-sheet-up {
  from { transform: translateY(100%); }
  to   { transform: translateY(0); }
}

/* Dimming backdrop — the dropdown has no backdrop element, so we paint one
   with a fixed ::before that fills the viewport BEHIND the sheet. A tap on
   it lands outside the pill + dropdown body → the existing document
   outside-tap listener (adminFilterDropdown.js) closes the sheet. */
.filter-dropdown:not([hidden])::before {
  content: "";
  position: fixed;
  inset: 0;
  z-index: -1;                     /* behind the sheet body, above the page */
  background: rgba(0, 0, 0, 0.45);
  animation: bs5-filter-backdrop-in 240ms ease;
}
@keyframes bs5-filter-backdrop-in {
  from { opacity: 0; }
  to   { opacity: 1; }
}

/* Grab handle — a CSS-only bar at the top of the sheet (mockup 15). Pure
   affordance; the existing footer "Done" / outside-tap / Escape dismiss it
   (we add no drag JS — staying CSS-only). */
.filter-dropdown:not([hidden])::after {
  content: "";
  flex: 0 0 auto;
  order: -1;  /* sheet is flex-column; pull the grab handle to the TOP (mockup 15) */
  align-self: center;
  width: var(--space-8);
  height: var(--space-0-5);
  margin: var(--space-2) 0 var(--space-3);
  border-radius: 999px;
  /* Same handle tone as the canonical fm-action-sheet handle bar. */
  background: var(--border-strong-light);
}

/* Option list scrolls within the sheet; the footer stays pinned. The
   desktop body gap/typography are kept — we only let it grow + scroll. */
.filter-dropdown:not([hidden]) .filter-dropdown__body {
  flex: 1 1 auto;
  overflow-y: auto;
  margin-bottom: var(--space-3);
  gap: var(--space-1);
}
/* Option rows → full-width 52px tap rows (mockup 15). The radio/checkbox
   keeps its native control (accent-color turquoise from the desktop rule);
   we only widen the row + lift the touch height. */
.filter-dropdown:not([hidden]) .audit-time-option,
.filter-dropdown:not([hidden]) .audit-actions-master,
.filter-dropdown:not([hidden]) .audit-actions-group__option {
  min-height: 44px;
  padding: var(--space-2) var(--space-2);
  border-radius: var(--radius-sm);
  font-size: var(--fs-sm);
}
/* The Select-All master row (Reports / Audit actions) reads as a pinned
   header row above the scrolling options. */
.filter-dropdown:not([hidden]) .audit-actions-master {
  flex: 0 0 auto;
  margin: 0 0 var(--space-2);
}
/* Search box inside a filter sheet (Audit actions, Username) spans full
   width — it already does, but give it breathing room as a sheet row. */
.filter-dropdown:not([hidden]) .filter-dropdown__search {
  flex: 0 0 auto;
  margin-bottom: var(--space-2);
}

/* Footer → pinned Clear (Reset) / Apply (Done) bar at the sheet bottom
   (mockup 15). The desktop row-reverse already puts the primary "Done"
   first visually; widen both for the 44px touch floor + even split. */
.filter-dropdown:not([hidden]) .filter-dropdown__footer {
  flex: 0 0 auto;
  margin-top: 0;
  padding-top: var(--space-3);
  gap: var(--space-2);
}
.filter-dropdown:not([hidden]) .filter-dropdown__footer .btn-primary {
  flex: 1 1 auto;
  min-height: 44px;
  justify-content: center;
}
.filter-dropdown:not([hidden]) .filter-dropdown__footer .filter-dropdown__reset {
  flex: 1 1 auto;
  min-height: 44px;
  border: var(--space-px) solid var(--hairline-dark);
  border-radius: 999px;
}
/* Reduced-motion → no slide-up / fade-in for the filter sheet + backdrop
   (nested under the ≤480px guard; ANDs with the width condition). */
@media (prefers-reduced-motion: reduce) {
  .filter-dropdown:not([hidden]),
  .filter-dropdown:not([hidden])::before {
    animation: none;
  }
}

/* ── BS.2  Table → card REFLOW SCAFFOLD ───────────────────────────
 * The structural heart of Option A. At ≤480px the semantic table stops
 * being a grid and becomes a vertical stack of cards — WITHOUT touching
 * the renderer. Each <tr> data row = one card; each <td> = a block line
 * inside it; the <thead> column labels are hidden (the card layout is
 * self-describing per the mockups). Status-section <tr>s become full-
 * width tone-coded group bars between cards.
 *
 * BS-3 layers the per-cell typography + the kebab onto this scaffold;
 * BS-1 only proves the reflow lands as a readable card column. */

/* Tables flow as blocks; the pager (auto-mounted sibling div) is left
 * to its own desktop rule (it already wraps fine — same pattern reused). */
.admin-table,
.admin-table tbody,
.admin-table tr,
.admin-table td {
  display: block;
  width: auto;
}

/* Column-label header row is redundant in card layout — hide it (the
 * sortable <thead> stays in the DOM for BS-6 to repurpose as a sort
 * sheet; here it's just visually removed). */
.admin-table thead {
  position: absolute;
  width: 1px;
  height: 1px;
  overflow: hidden;
  clip: rect(0 0 0 0);
  white-space: nowrap;
}

/* Each data row = a card. Spacer + empty rows opt out (handled below).
 * BS-3 polishes the BS-1 scaffold: a touch more interior padding +
 * relative positioning so the trailing action cell can be slotted to a
 * clean bottom-right corner (the future BS-4 kebab drops into the same
 * slot — until then the icon row lives there, see BS.4 below). */
.admin-table tbody tr:not(.admin-mod-section):not(.admin-table__spacer-row):not(.admin-table__empty-row) {
  position: relative;
  background: var(--cream-surface);
  border: 1px solid var(--border-on-light);
  border-left-width: var(--space-0-5);   /* tone rule painted by section rules below */
  border-radius: var(--radius-md);
  padding: var(--space-3) var(--space-3) var(--space-2) var(--space-4);
  margin: 0 0 var(--space-2);
  box-shadow: var(--shadow-card);
}
/* Neutralise the desktop zebra-stripe + hover tint inside the card layout
 * (cards already separate via gap + border; zebra would read as noise). */
.admin-table tbody tr:nth-child(even) { background: var(--cream-surface); }

/* Cells become stacked block lines; clear the desktop cell padding +
 * right-alignment of the trailing (actions) cell. */
.admin-table tbody td {
  padding: var(--space-0-5) 0;
  border: 0;
  text-align: left;
  white-space: normal;
}

/* BS-3 — TITLE CELL (the first <td>). The renderer puts the underlined
 * turquoise title + optional TEST pill + (hidden) mod-reason span here.
 * Promote it to the visual anchor of the card: larger, Cormorant-feel
 * display weight, a little bottom breathing room before the meta lines.
 * The desktop turquoise underline + cancelled colour are inherited — we
 * only resize + space them for the card. */
.admin-table tbody td:first-child {
  margin-bottom: var(--space-1);
}
.admin-table tbody .admin-row__title,
.admin-table tbody .admin-row__title-link {
  font-size: var(--fs-md);
  line-height: 1.3;
  /* Title can run long (event names wrap freely) — let it. */
  white-space: normal;
  word-break: break-word;
}

/* BS-3 — META lines (Tour name / Subcategory / Location+flag / Author).
 * The desktop renders these as separate cells; in the card they stack as
 * small muted lines under the title. Tighten the size to the meta scale
 * and keep flag images inline-aligned with their city text. */
.admin-table tbody .admin-row__meta {
  font-size: var(--fs-xs);
  color: var(--text-muted);
  line-height: 1.5;
}
.admin-table tbody .admin-row__meta .feed-list__flag {
  vertical-align: -2px;
}

/* BS-3 — WHEN cell. Desktop stacks date over a muted time; in the narrow
 * card we keep that stack but inline it to read as one compact meta line
 * (date then time on the same row saves a line of height). */
.admin-table tbody .admin-row__when {
  font-size: var(--fs-xs);
  line-height: 1.4;
}
.admin-table tbody .admin-row__when .admin-when__date {
  display: inline;
  font-size: var(--fs-xs);
  /* Value hierarchy (Petřin nález 2026-06-05 „šedá změť") — the date is
     a VALUE: bright against the mist "When" label + muted secondaries. */
  color: var(--text-dark);
}
.admin-table tbody .admin-row__when .admin-when__time {
  display: inline;
  margin-top: 0;
  margin-left: var(--space-1-5);
  font-size: var(--fs-xs);
  color: var(--text-dark);
}

/* Spacer rows (height-stabiliser padding for paged desktop tables) are
 * meaningless in a card list — collapse them so a short page doesn't
 * leave a stack of empty cards. */
.admin-table tbody tr.admin-table__spacer-row { display: none; }

/* Empty-state row — keep it as a centred block, not a card. */
.admin-table tbody tr.admin-table__empty-row {
  display: block;
  background: transparent;
  border: 0;
  box-shadow: none;
  padding: var(--space-6) var(--space-3);
}

/* Loading row (M14 A11c dots) — a STATUS, not a record: strip the card
 * chrome so it never reads as a broken empty card (Petřin nález
 * 2026-06-05 „karta vypadá prázdná"). The per-tab card blocks below
 * exclude .admin-queue-pending-row from their flex/::before chip rules. */
.admin-table tbody tr.admin-queue-pending-row {
  display: block;
  background: transparent;
  border: 0;
  box-shadow: none;
  padding: var(--space-6) var(--space-3);
}
.admin-table tbody tr.admin-queue-pending-row td {
  display: block;
  padding: 0;
}
/* Phone visibility boost — the desktop placeholder (6px dots @ 0.35 +
 * small muted label) disappears on the dark Noir surface. Larger dots,
 * stronger opacity, mist label so "Loading…" is legible at arm's length. */
.admin-table .admin-queue-pending__dots span {
  width: 8px;
  height: 8px;
  opacity: 0.55;
}
.admin-table .admin-queue-pending__label {
  font-size: var(--fs-md);
  color: var(--text-mist);
}
.admin-table tbody tr.admin-table__empty-row td.admin-table__empty-cell {
  text-align: center;
  font-size: var(--fs-sm);
}

/* ── BS.3  Status group bars ──────────────────────────────────────
 * The section header <tr> (PENDING / APPROVED / REJECTED) becomes a
 * full-width tone-coded bar between the cards. SAME tones as desktop
 * (pending=amber · approved=teal · rejected=danger-brick) so a moderator
 * reads the identical hierarchy on web + phone (m14-overview §2). The
 * cards that follow each bar inherit that tone on their left rule. */
.admin-table tbody tr.admin-mod-section {
  display: block;
  margin: var(--space-4) 0 var(--space-2);
}
.admin-table tbody tr.admin-mod-section th {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  flex-wrap: wrap;
  width: 100%;
  /* Full-width tone-coded bar with a 2px tone left rule so the divider
     visually parents the cards beneath it (mockups 05 + 07). The bar
     keeps a tight, scannable rhythm; the rule colour comes from the
     per-kind border-left overrides below. */
  padding: var(--space-2) var(--space-3);
  border-left: var(--space-0-5) solid currentColor;
  border-radius: 0 var(--radius-sm) var(--radius-sm) 0;
  font-size: var(--fs-xs);
  letter-spacing: 0.16em;
  /* tone bg/colour inherited from the desktop --pending/--approved/
     --rejected rules — MEANING preserved (pending=amber, approved=teal,
     rejected=brick), only the container shape changes. The desktop th
     top/bottom borders would read as stray hairlines on the bar, so null
     them; the left rule + tinted bg carry the tone instead. */
  border-top: 0;
  border-bottom: 0;
}
/* Left-rule tone per section (currentColor = the desktop tone text colour,
   so the rule matches the label automatically — pending amber, approved
   teal, rejected brick). Cards beneath inherit the same tone (below). */
.admin-table tbody tr.admin-mod-section .admin-mod-section__icon {
  font-size: var(--fs-md);
  line-height: 1;
}
/* M12D-3 — the renderer injects an EMOJI (📋 / ✅ / 🚫) into the section icon
   span (shared desktop JS, can't be changed without touching desktop). On a
   phone the emoji reads off-tone + bumpy (it ignores the section's copper/
   teal/danger currentColor and renders its own colour). Per mockups 01/05/07
   + ui-standards §7.3b "crisp SVG mask, never emoji": blank the glyph and
   paint a tone-coded monochrome SVG in its place via a mask (currentColor →
   the section tone). Mobile-only, CSS-only — desktop keeps its emoji. */
.admin-table tbody tr.admin-mod-section .admin-mod-section__icon {
  position: relative;
  display: inline-grid;
  place-items: center;
  width: var(--space-4);
  height: var(--space-4);
  font-size: 0;                      /* hide the source emoji glyph */
  color: inherit;                    /* tone = the section's currentColor */
}
.admin-table tbody tr.admin-mod-section .admin-mod-section__icon::before {
  content: "";
  width: 100%;
  height: 100%;
  background: currentColor;
  -webkit-mask: var(--bs-section-glyph, none) center / contain no-repeat;
          mask: var(--bs-section-glyph, none) center / contain no-repeat;
}
/* Pending = flame (queue waiting for review); approved = spark/plus; rejected
   = circle-slash. SVGs follow the section currentColor through the mask. */
.admin-table tbody tr.admin-mod-section--pending .admin-mod-section__icon {
  --bs-section-glyph: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='%23000'%3E%3Cpath d='M12 2c1 3-1 4-2 6-1.2 2.4-.5 4 1 4 1.6 0 2-1.4 2-2.6 1.5 1 3 3 3 5.6a6 6 0 1 1-11.2-3C6.8 9 9 7 9.5 4c.8 1 1 2.2 1 3 .8-1.6 1.5-3.6 1.5-5z'/%3E%3C/svg%3E");
}
.admin-table tbody tr.admin-mod-section--approved .admin-mod-section__icon {
  --bs-section-glyph: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2.4' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M5 13l4 4L19 7'/%3E%3C/svg%3E");
}
.admin-table tbody tr.admin-mod-section--rejected .admin-mod-section__icon {
  --bs-section-glyph: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2.2' stroke-linecap='round'%3E%3Ccircle cx='12' cy='12' r='8.5'/%3E%3Cpath d='M6 6l12 12'/%3E%3C/svg%3E");
}
/* Count sits right after the label, lighter (mockup "(1)" / "(32)"). */
.admin-table tbody tr.admin-mod-section .admin-mod-section__count {
  /* No extra margin — the flex `gap` (--space-2) already separates the count
     from the label. The old margin-left stacked ON TOP of the gap, pushing the
     number too far from "PENDING"/"APPROVED" (Petřin pokyn 2026-05-31). Now the
     label→count rhythm matches the icon→label gap. */
  margin-left: 0;
  font-weight: 600;
  /* Full opacity + inherit the bar's bright tone so the "(1)" / "(5)" count
     reads as clearly as its label on the dark surface (Petřin pokyn 2026-05-28
     — "čísla nejdou vidět" + "moc malá"). Bigger than the label's --fs-xs and
     drop the label's wide tracking so the number isn't tiny + spread out. */
  opacity: 1;
  color: inherit;
  font-size: var(--fs-sm);
  letter-spacing: 0;
}
/* Collapse chevron (rejected section) pushes to the bar's right edge. */
.admin-table tbody tr.admin-mod-section .admin-mod-section__chevron {
  margin-left: auto;
  float: none;
}
/* History note (rejected) drops to its own full-width line under the
   label so it never crowds the count on a narrow bar (mockup 07).
   MIST tone per m14-per-tab §7 — inheriting the bar's currentColor made
   it red-on-red and unreadable (Petřin nález 2026-06-05; shared helper
   = same hint on Concerts/Online/Landmarks rejected bars). The th's
   uppercase + 0.16em tracking also inherited — undo both so the note
   reads as a quiet sentence, not dimmed red small-caps. */
.admin-table tbody tr.admin-mod-section .admin-mod-section__hint {
  flex-basis: 100%;
  margin-top: var(--space-1);
  opacity: 1;
  /* Same token as .admin-pane__subtitle — Petřina volba 2026-06-05. */
  color: var(--text-muted);
  text-transform: none;
  letter-spacing: 0.02em;
}

/* Card left-rule tone follows the group it sits under. The renderer tags
 * each data row with data-section-row="{kind}" (adminModerationHelpers);
 * we reuse that hook so the tone-coding needs NO JS change. */
.admin-table tbody tr[data-section-row="pending"]  { border-left-color: var(--accent-copper); }
.admin-table tbody tr[data-section-row="approved"] { border-left-color: var(--accent-turquoise); }
.admin-table tbody tr[data-section-row="rejected"] { border-left-color: var(--danger); }

/* Audit log has no status sections, so its cards have no per-section tone.
   Give them the iris left rule the audit tab carries throughout (mockup 03 —
   audit = iris world). Petřin pokyn 2026-05-28. */
.admin-table--audit tbody tr { border-left-color: var(--accent-violet); }

/* Status group bar TONE on the dark mobile surface. The desktop rules colour
   PENDING with a dark burnt-orange hex tuned for the light Frost bg —
   on Noir it goes invisible ("pending je neviditelná, barvy selhávají"). Repaint
   the three bars with the BRIGHT Noir accent tokens the fan Yours tab uses, so
   the label + its left rule (currentColor) read on dark. Petřin pokyn 2026-05-28
   — vzít barvy z Yours; totéž rejected. */
.admin-table tbody tr.admin-mod-section--pending th {
  color: var(--accent-copper);
  background: color-mix(in srgb, var(--accent-copper) 12%, transparent);
  border-top-color: color-mix(in srgb, var(--accent-copper) 32%, transparent);
  border-bottom-color: color-mix(in srgb, var(--accent-copper) 32%, transparent);
}
.admin-table tbody tr.admin-mod-section--approved th {
  color: var(--accent-turquoise);
  background: color-mix(in srgb, var(--accent-turquoise) 10%, transparent);
}
.admin-table tbody tr.admin-mod-section--rejected th {
  color: var(--danger-text);
  background: color-mix(in srgb, var(--danger) 12%, transparent);
}

/* ── BS.3b  Status pill on the card (the per-row badge) ────────────
 * The renderer drops a .feed-list__badge (status) + optionally a
 * .feed-list__badge--cancelled into the Status cell. Desktop already
 * tone-codes these (upcoming/live/today=teal-green, past=muted,
 * cancelled=danger-tint). In the card we just give the Status cell a
 * little top gap so the pill sits as its own meta line — no recolour,
 * the canonical badge primitive is reused as-is (no new variant). */
.admin-table tbody td .feed-list__badge {
  margin-top: var(--space-0-5);
}

/* Cancelled treatment per ADR-169 (override #3): a cancelled row differs
 * by TREATMENT — strikethrough title + danger halo — NOT a brick tint of
 * the whole card. The desktop rule recolours .admin-row__title on
 * .is-cancelled; here we complete the ADR-169 treatment by adding the
 * strikethrough on the title AND the danger-halo left rule + ring on the
 * card, so the cancelled state reads as "did not happen" without
 * repainting the whole card as a delete. */
.admin-table tbody tr.is-cancelled {
  border-left-color: var(--danger);
  box-shadow: var(--shadow-card), 0 0 0 1px rgba(var(--danger-rgb), 0.28);
}
.admin-table tbody tr.is-cancelled .admin-row__title {
  text-decoration: line-through;
}

/* ── BS — Reports card (M12D, 2026-05-29) ─────────────────────────
 * The Reports queue (#admin-table) is the only admin table that wasn't a
 * structured card on the phone — its renderer emitted bare cells, so the
 * generic BS-3 fallback stacked them as one undifferentiated white column
 * ("změť bílého písma"). The renderer now tags each cell with a reports class
 * (.admin-reports__reason / __author / __content / __type / __filed); here we
 * reflow them into the canonical card from mockup 01-reports.png + m14-per-tab
 * §1: a type-chip + status-pill TOP LINE, the report REASON as the title, then
 * labelled muted meta. Scoped to #admin-table so no other tab is touched.
 *
 * NOTE the labels read "Author" not "Reporter": the @nick on a report row is
 * the author of the REPORTED content (= report.pin.nick), not the reporter —
 * the mockup's "Reporter" label was wrong (mockup = visual source, not labels).
 */

/* Reflow as a flex column so `order` can lift the type+status line above the
 * title WITHOUT touching DOM order (desktop column order stays intact). The
 * hidden checkbox cell + absolute kebab ignore `order` and stay out of flow. */
#admin-table tbody tr:not(.admin-table__spacer-row):not(.admin-table__empty-row):not(.admin-queue-pending-row) {
  display: flex;
  flex-direction: column;
}
#admin-table tbody td.admin-reports__type    { order: 1; }
#admin-table tbody td.admin-reports__reason  { order: 2; }
#admin-table tbody td.admin-reports__author  { order: 3; }
#admin-table tbody td.admin-reports__filed   { order: 4; }
#admin-table tbody td.admin-reports__content { order: 5; }

/* Top line — type chip + status pill side by side. Right padding clears the
 * corner kebab (BS.4) so the line never runs under it. */
#admin-table tbody td.admin-reports__type {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  flex-wrap: wrap;
  padding-right: calc(var(--space-8) + var(--space-2));
  margin-bottom: var(--space-1);
}

/* Title = the report reason — the card's visual anchor (turquoise display
 * weight, per the mockup). Not a link here (the kebab opens the detail), so
 * no underline / link cursor — just the prominent tone + size. */
#admin-table tbody td.admin-reports__reason .admin-row__title-text {
  font-size: var(--fs-md);
  font-weight: var(--fw-semibold);
  line-height: 1.3;
  color: var(--accent-turquoise);
  word-break: break-word;
}

/* Meta lines — small, muted, each prefixed by its own label. */
#admin-table tbody td.admin-reports__author,
#admin-table tbody td.admin-reports__content,
#admin-table tbody td.admin-reports__filed {
  font-size: var(--fs-xs);
  color: var(--text-muted);
  line-height: 1.5;
}
/* Mobile-only field label (Author / Gathering name / Filed). Re-shown here
 * (style.css hides it for desktop, where the column headers carry context). */
.admin-cell-label {
  display: inline;
  font-weight: var(--fw-semibold);
  color: var(--text-mist);
  margin-right: var(--space-1);
}
/* The reported content keeps the renderer's truncation; the value reads as
 * primary text, an empty "(none)" as muted italic. */
#admin-table tbody td.admin-reports__content .admin-cell-content {
  color: var(--text-dark);
}
#admin-table tbody td.admin-reports__content .admin-cell-content--empty {
  font-style: italic;
  color: var(--text-muted);
}

/* Status pill — same lozenge as the type chip (it carries the canonical
 * .pin-type-chip class for IDENTICAL size; see adminUI.js). We only re-show it
 * on the card (style.css hides it for desktop) + set the lifecycle tone:
 * pending=copper, resolved=teal, dismissed=muted — matching the admin status
 * section bars (BS.3) so web + phone read the same hierarchy. These tones are
 * lifecycle status, NOT entity colours (entity = the violet type chip). */
.admin-reports__type .admin-status-pill {
  display: inline-flex;
}
.admin-status-pill--pending {
  color: var(--accent-copper);
  background: rgba(var(--accent-copper-rgb), 0.16);
  border-color: rgba(var(--accent-copper-rgb), 0.3);
}
.admin-status-pill--resolved {
  color: var(--accent-turquoise);
  background: rgba(var(--accent-turquoise-rgb), 0.14);
  border-color: rgba(var(--accent-turquoise-rgb), 0.28);
}
.admin-status-pill--dismissed {
  color: var(--text-muted);
  background: color-mix(in srgb, var(--text-muted) 16%, transparent);
  border-color: color-mix(in srgb, var(--text-muted) 30%, transparent);
}

/* Card left-rule tone by report status (the row already carries
 * .admin-row--{status} from the renderer). Mirrors the status-pill tone so
 * the queue reads at a glance — pending cards lead with the copper rule. */
#admin-table tbody tr.admin-row--pending   { border-left-color: var(--accent-copper); }
#admin-table tbody tr.admin-row--resolved  { border-left-color: var(--accent-turquoise); }
#admin-table tbody tr.admin-row--dismissed { border-left-color: var(--hairline-dark); }

/* ── M15.X-ADMIN-HEADER follow-up (2026-06-05) — Test data filter pills
 * flow as one wrapping row (mockup 12), same as the BS-5 `.admin-toolbar`
 * lane — the Test data toolbar has its own class so the BS-5 rule never
 * reached it and the pills stacked full-width. */
.admin-test-data__toolbar {
  display: flex;
  /* style.css has an older @media(640px) rule stacking this toolbar into
     a column — restore the row flow the mockup shows. */
  flex-direction: row;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-2);
}
.admin-test-data__toolbar > .filter-pill-wrap {
  flex: 0 0 auto;
}

/* ── M15.X-ADMIN-HEADER follow-up (2026-06-05) — TEST DATA per-tab card
 * (admin-mobile handoff mockups/12-test-data.png). Until now the Test
 * data rows only had the generic BS-1 stacked-cells scaffold — six bare
 * lines with stray "—" dashes (Petřin screenshot 2026-06-05 „tabulky se
 * rozhodily"; they were never composed). Mirror of the Reports per-tab
 * pattern above, scoped to #admin-test-data-table:
 *   type chip + status chip on the TOP line (status right-aligned before
 *   the kebab corner) · title as the anchor · one muted meta line
 *   "Author @x · Date y" · empty details ("—") hidden entirely. */
#admin-test-data-table tbody tr:not(.admin-table__spacer-row):not(.admin-table__empty-row):not(.admin-queue-pending-row) {
  display: flex;
  flex-direction: row;
  flex-wrap: wrap;
  align-items: baseline;
}
#admin-test-data-table tbody td.admin-test-data__col-type {
  order: 1;
  flex: 1 0 100%;
  /* Clear the absolute status chip + kebab corner so the type chip never
     runs underneath them. */
  padding-right: calc(var(--space-8) + var(--space-8));
  margin-bottom: var(--space-1);
}
#admin-test-data-table tbody td.admin-test-data__col-status {
  /* Status chip docks top-right, leaving the corner slot for the kebab. */
  position: absolute;
  top: var(--space-3);
  right: calc(var(--space-8) + var(--space-2));
  padding: 0;
}
#admin-test-data-table tbody td.admin-test-data__col-title {
  order: 2;
  flex: 1 0 100%;
  margin-bottom: var(--space-1);
}
#admin-test-data-table tbody td.admin-test-data__col-author {
  order: 3;
  flex: 0 0 auto;
  /* Value hierarchy — nick = lavender (M13 identity token), parity with
     the Landmarks card + fan surfaces. */
  color: var(--nick-color);
}
#admin-test-data-table tbody td.admin-test-data__col-date {
  order: 4;
  flex: 0 0 auto;
  color: var(--text-dark);
}
/* "Author" / "· Date" labels — CSS-only (the renderer emits no label
 * spans here, unlike Reports). EN literals are safe: the app is ENG-LOCK
 * and this is the admin-only surface; swap to renderer spans if Test
 * data ever grows i18n labels. */
#admin-test-data-table tbody td.admin-test-data__col-author::before {
  content: "Author ";
  font-weight: var(--fw-semibold);
  color: var(--text-mist);
}
#admin-test-data-table tbody td.admin-test-data__col-date::before {
  content: "· Date ";
  font-weight: var(--fw-semibold);
  color: var(--text-mist);
  margin-left: var(--space-1-5);
}
/* Empty details = a lone "—" span; hide the whole line (mockup card has
 * no dash filler rows). A real details text (no inner span) stays. */
#admin-test-data-table tbody td.admin-test-data__col-details {
  order: 5;
  flex: 1 0 100%;
}
#admin-test-data-table tbody td.admin-test-data__col-details:has(> .admin-row__meta:only-child) {
  display: none;
}
#admin-test-data-table tbody td.admin-test-data__col-actions {
  order: 6;
}

/* ── M15.X-ADMIN-HEADER follow-up (2026-06-05) — PER-TAB CARDS for the
 * remaining tabs (admin-mobile handoff m14-per-tab.md + mockups 04-07,
 * 09-10). Until now only Reports (+ Test data above) had a composed
 * card; Tours / Concerts / Online / Landmarks / Crew / Users still sat
 * on the bare BS-1 stacked-cells scaffold (Petřin pokyn 2026-06-05
 * „ostatních záložek se to týká taky"). Same CSS-only philosophy: flex
 * + order re-composition of the EXISTING cells, no renderer change.
 * Field labels are CSS literals (EN, ENG-LOCK admin surface — same
 * trade-off as the Test data block above). */

/* Shared card frame — flex so `order` can re-arrange without touching
 * the DOM. Section header rows (PENDING/APPROVED bars) are excluded. */
#admin-concerts-table tbody tr:not(.admin-mod-section):not(.admin-table__spacer-row):not(.admin-table__empty-row):not(.admin-queue-pending-row),
#admin-online-table tbody tr:not(.admin-mod-section):not(.admin-table__spacer-row):not(.admin-table__empty-row):not(.admin-queue-pending-row),
#admin-tours-table tbody tr:not(.admin-mod-section):not(.admin-table__spacer-row):not(.admin-table__empty-row):not(.admin-queue-pending-row),
#admin-sigplaces-table tbody tr:not(.admin-mod-section):not(.admin-table__spacer-row):not(.admin-table__empty-row):not(.admin-queue-pending-row),
#admin-moderators-table tbody tr:not(.admin-table__spacer-row):not(.admin-table__empty-row):not(.admin-queue-pending-row),
#admin-users-table tbody tr:not(.admin-table__spacer-row):not(.admin-table__empty-row):not(.admin-queue-pending-row) {
  display: flex;
  flex-direction: row;
  flex-wrap: wrap;
  align-items: baseline;
}

/* Entity chip on the card's top line. Tours + Landmarks have no type
 * cell at all → paint the chip as a ::before flex item on the row;
 * Concerts/Online repurpose their tour/subcategory cell as the chip
 * ("Concert · Frisson", "Online · Livestream" — mockups 05/06). */
#admin-tours-table tbody tr:not(.admin-mod-section):not(.admin-table__spacer-row):not(.admin-table__empty-row):not(.admin-queue-pending-row)::before,
#admin-sigplaces-table tbody tr:not(.admin-mod-section):not(.admin-table__spacer-row):not(.admin-table__empty-row):not(.admin-queue-pending-row)::before,
#admin-concerts-table tbody td:nth-child(2),
#admin-online-table tbody td:nth-child(2) {
  order: 1;
  flex: 0 0 auto;
  align-self: flex-start;
  font-size: var(--fs-2xs);
  font-weight: var(--fw-semibold);
  text-transform: uppercase;
  letter-spacing: 0.08em;
  color: var(--accent-turquoise);
  background: rgba(var(--accent-turquoise-rgb), 0.10);
  border: var(--space-px) solid rgba(var(--accent-turquoise-rgb), 0.28);
  border-radius: 999px;
  padding: var(--space-0-5) var(--space-2);
  margin-bottom: var(--space-1-5);
  /* keep clear of the absolute status pill + kebab corner */
  max-width: calc(100% - var(--space-8) - var(--space-8));
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
#admin-tours-table tbody tr:not(.admin-mod-section):not(.admin-table__spacer-row):not(.admin-table__empty-row):not(.admin-queue-pending-row)::before {
  content: "Tour";
}
#admin-sigplaces-table tbody tr:not(.admin-mod-section):not(.admin-table__spacer-row):not(.admin-table__empty-row):not(.admin-queue-pending-row)::before {
  content: "Landmark";
  color: var(--accent-copper);
  background: rgba(var(--accent-copper-rgb), 0.10);
  border-color: rgba(var(--accent-copper-rgb), 0.30);
}
#admin-concerts-table tbody td:nth-child(2)::before { content: "Concert · "; }
#admin-online-table tbody td:nth-child(2)::before   { content: "Online · "; }

/* Status pill (Concerts/Online status cell) docks top-right before the
 * kebab corner — the cell already renders the canonical badge pill.
 * Target the STATUS COLUMN by position (4th), not by `:has(> badge)` —
 * cancelled rows carry a CANCELLED badge inside the TITLE cell too, and
 * the content-based selector yanked the whole title out of flow (red
 * strikethrough title over the type chip — Petřin screenshot 2026-06-05). */
#admin-concerts-table tbody td:nth-child(4),
#admin-online-table tbody td:nth-child(4) {
  position: absolute;
  top: var(--space-3);
  right: calc(var(--space-8) + var(--space-2));
  padding: 0;
}

/* Pill priority on a cancelled card (mockup 06 + m14-row-card §4):
 * cancellation supersedes past/upcoming — mobile shows ONLY the red
 * CANCELLED pill. Hide the lifecycle pill and dock the title cell's
 * CANCELLED badge into the freed top-right slot instead. */
#admin-concerts-table tbody tr.is-cancelled td:nth-child(4),
#admin-online-table tbody tr.is-cancelled td:nth-child(4) {
  display: none;
}
#admin-concerts-table tbody tr.is-cancelled td:first-child > .feed-list__badge--cancelled,
#admin-online-table tbody tr.is-cancelled td:first-child > .feed-list__badge--cancelled {
  position: absolute;
  top: var(--space-3);
  right: calc(var(--space-8) + var(--space-2));
}

/* Title = card anchor, full row. */
#admin-concerts-table tbody td:first-child,
#admin-online-table tbody td:first-child,
#admin-tours-table tbody td:first-child,
#admin-sigplaces-table tbody td:first-child {
  order: 2;
  flex: 1 0 100%;
  margin-bottom: var(--space-1);
}

/* When line — labelled, one row (mockup 05 "When 9 Oct 2026 · 21:30"). */
#admin-concerts-table tbody td.admin-row__when,
#admin-online-table tbody td.admin-row__when {
  order: 3;
  flex: 1 0 100%;
}
#admin-concerts-table tbody td.admin-row__when::before,
#admin-online-table tbody td.admin-row__when::before {
  content: "When ";
  font-weight: var(--fw-semibold);
  color: var(--text-mist);
  margin-right: var(--space-1);
}

/* Tours — description clamps to 2 lines; Start/End as one labelled meta
 * row; concerts count docks top-right as the teal tally (mockup 04).
 * Description = the ONE muted secondary line; dates read as bright
 * values so the two stop blending (Petřin nález „datum, popis, nijak
 * od sebe odlišitelné"). */
#admin-tours-table tbody td:nth-child(2) {
  order: 3;
  flex: 1 0 100%;
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
  color: var(--text-muted);
}
#admin-tours-table tbody td:nth-child(3),
#admin-tours-table tbody td:nth-child(4) {
  order: 4;
  flex: 0 0 auto;
  font-size: var(--fs-xs);
  color: var(--text-dark);
}
#admin-tours-table tbody td:nth-child(3)::before {
  content: "Start ";
  font-weight: var(--fw-semibold);
  color: var(--text-mist);
}
#admin-tours-table tbody td:nth-child(4) {
  order: 5;
}
#admin-tours-table tbody td:nth-child(4)::before {
  content: "· End ";
  font-weight: var(--fw-semibold);
  color: var(--text-mist);
  margin-left: var(--space-1-5);
}
/* Empty cells (lone "—": no description / open-ended tour) hide on the
 * phone card — same .admin-cell--empty idiom as Landmarks. */
#admin-tours-table tbody td.admin-cell--empty { display: none; }
#admin-tours-table tbody td:nth-child(5) {
  position: absolute;
  top: var(--space-3);
  right: calc(var(--space-8) + var(--space-2));
  padding: 0;
  font-size: var(--fs-2xs);
  font-weight: var(--fw-semibold);
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--accent-turquoise);
}
#admin-tours-table tbody td:nth-child(5)::after { content: " concerts"; }
#admin-tours-table tbody td:nth-child(5) .admin-row__meta { display: none; }

/* Landmarks — ONE flowing meta line (m14-row-card §5: dot-separated
 * inline pairs, "stay flat") instead of three stacked one-word rows
 * (Petřin nález 2026-06-05 „řádky v jednom úzkém sloupci"): flag+city ·
 * Author @x · Proposed date. Wraps naturally when long. Empty cells
 * (lone "—") carry .admin-cell--empty from the renderer — hidden on the
 * phone card (desktop keeps the dash for column alignment).
 * VALUE HIERARCHY (Petřin nález 2026-06-05 „šedá změť"): labels mist,
 * values --text-dark, identity tones from the M13 system the fan side
 * already speaks — place = --location-color (sand), nick = --nick-color
 * (lavender). One glance separates the kinds of info. */
#admin-sigplaces-table tbody td:nth-child(2),
#admin-sigplaces-table tbody td:nth-child(3),
#admin-sigplaces-table tbody td:nth-child(4) {
  flex: 0 0 auto;
  font-size: var(--fs-xs);
  color: var(--text-dark);
}
#admin-sigplaces-table tbody td:nth-child(2) { color: var(--location-color); }
#admin-sigplaces-table tbody td:nth-child(3) { color: var(--nick-color); }
#admin-sigplaces-table tbody td:nth-child(2) { order: 3; }
#admin-sigplaces-table tbody td:nth-child(3) { order: 4; }
#admin-sigplaces-table tbody td:nth-child(3)::before {
  content: "· Author ";
  font-weight: var(--fw-semibold);
  color: var(--text-mist);
  margin-left: var(--space-1-5);
}
#admin-sigplaces-table tbody td:nth-child(4) { order: 5; }
#admin-sigplaces-table tbody td:nth-child(4)::before {
  content: "· Proposed ";
  font-weight: var(--fw-semibold);
  color: var(--text-mist);
  margin-left: var(--space-1-5);
}
#admin-sigplaces-table tbody td.admin-cell--empty { display: none; }

/* Crew — identity (avatar + nickname) leads; role chip docks top-right;
 * permission chips flow as their own row (mockup 09 — chips exist in
 * the DOM already, only placement changes). */
#admin-moderators-table tbody td:first-child {
  order: 1;
  flex: 1 0 100%;
  padding-right: calc(var(--space-8) + var(--space-8));
  margin-bottom: var(--space-1);
}
#admin-moderators-table tbody td:nth-child(2) {
  position: absolute;
  top: var(--space-3);
  right: calc(var(--space-8) + var(--space-2));
  padding: 0;
}
#admin-moderators-table tbody td:nth-child(3) {
  order: 2;
  flex: 1 0 100%;
}

/* Users — nickname leads; "Banned · 1 day" pill docks top-right; meta
 * lines labelled Reason / Expires / By (mockup 10). */
#admin-users-table tbody td:first-child {
  order: 1;
  flex: 1 0 100%;
  padding-right: calc(var(--space-8) + var(--space-8));
  margin-bottom: var(--space-1);
}
#admin-users-table tbody td:nth-child(2) {
  position: absolute;
  top: var(--space-3);
  right: calc(var(--space-8) + var(--space-2));
  padding: var(--space-0-5) var(--space-2);
  font-size: var(--fs-2xs);
  font-weight: var(--fw-semibold);
  text-transform: uppercase;
  letter-spacing: 0.08em;
  color: var(--accent-mine);
  background: rgba(var(--accent-mine-rgb), 0.12);
  border: var(--space-px) solid rgba(var(--accent-mine-rgb), 0.30);
  border-radius: 999px;
}
#admin-users-table tbody td:nth-child(2)::before { content: "Banned · "; }
#admin-users-table tbody td:nth-child(4) { order: 2; }
#admin-users-table tbody td:nth-child(3) { order: 3; }
#admin-users-table tbody td:nth-child(5) { order: 4; }
#admin-users-table tbody td:nth-child(3),
#admin-users-table tbody td:nth-child(4),
#admin-users-table tbody td:nth-child(5) {
  flex: 1 0 100%;
  font-size: var(--fs-xs);
  /* Value hierarchy — ban values read bright against mist labels;
     "By" = moderator nick → lavender identity token. */
  color: var(--text-dark);
}
#admin-users-table tbody td:nth-child(5) { color: var(--nick-color); }
#admin-users-table tbody td:nth-child(4)::before {
  content: "Reason ";
  font-weight: var(--fw-semibold);
  color: var(--text-mist);
}
#admin-users-table tbody td:nth-child(3)::before {
  content: "Expires ";
  font-weight: var(--fw-semibold);
  color: var(--text-mist);
}
#admin-users-table tbody td:nth-child(5)::before {
  content: "By ";
  font-weight: var(--fw-semibold);
  color: var(--text-mist);
}

/* ── M15.X-ADMIN-HEADER follow-up (2026-06-05) — LIMITS tab, phone
 * composition (admin-mobile handoff mockups/08-limits.png + m14-per-tab
 * §8). The sections already stack; what the mockup adds on a phone is
 * the USAGE METER — a thin progress bar under each metered line (width
 * = --usage-pct, set by adminLimitsAI.js _setMeter). Save stays in the
 * sticky band (ADR-263 supersedes the handoff's commit-on-blur); the
 * service headings keep the tracked field-label token (Petřin pokyn
 * 2026-05-28 supersedes the mockup's italic serif — dohody > mockupy). */
#admin-pane-limits .admin-usage__meter::after {
  content: "";
  display: block;
  margin-top: var(--space-1-5);
  height: var(--space-1);
  border-radius: 999px;
  /* fill (turquoise, width --usage-pct) painted over the muted track */
  background:
    linear-gradient(90deg, var(--accent-turquoise), var(--accent-turquoise))
      0 0 / var(--usage-pct, 0%) 100% no-repeat,
    color-mix(in srgb, var(--text-muted) 22%, transparent);
}
/* Headline AWS total — the mockup's hero figure: lift the line so the
 * month's burn reads first, its meter slightly stronger. */
#admin-pane-limits .admin-usage__total {
  font-size: var(--fs-lg);
}
#admin-pane-limits .admin-usage__total.admin-usage__meter::after {
  height: var(--space-1-5);
}
/* Touch comfort — number inputs + checkbox rows to the 44px touch
 * floor (P-030 idiom used throughout this file). */
#admin-pane-limits .admin-limits__row input[type="number"] {
  min-height: 44px;
}
#admin-pane-limits .admin-limits__row--checkbox {
  align-items: center;
  min-height: 44px;
}

/* ── M15.X-ADMIN-HEADER follow-up (2026-06-05) — CHART tab, phone.
 * No mockup (the tab post-dates the admin-mobile handoff — ui-standards
 * §0.5 missing-mockup governance): the desktop 600px breakpoint already
 * stacks title above a full-width input; here just the 44px touch floor
 * + row air so consecutive URL fields don't fat-finger. */
#admin-pane-chart .admin-chart-row__input {
  min-height: 44px;
}
#admin-pane-chart .admin-chart-row {
  padding: var(--space-2) 0 var(--space-3);
}

/* Action cells sort last in every composed card (the kebab is absolute
 * anyway; this only parks the hidden cell out of the flow order). */
#admin-concerts-table tbody td:has(> .admin-actions),
#admin-online-table tbody td:has(> .admin-actions),
#admin-tours-table tbody td:has(> .admin-actions),
#admin-sigplaces-table tbody td:has(> .admin-actions),
#admin-moderators-table tbody td.admin-actions,
#admin-users-table tbody td.admin-actions {
  order: 9;
}

/* ── BS.4  Action cluster → single kebab → fm-action-sheet ────────
 * BS-4 collapses the inline icon cluster behind ONE kebab "⋮" that opens
 * the reused fan action sheet (fm-action-sheet). At ≤480px:
 *   • the inline .admin-actions__icon buttons are HIDDEN (they stay in the
 *     DOM — adminMobileActions.js mirrors them into the sheet and triggers
 *     each one's .click(), so logic / permission / confirm are untouched);
 *   • a single .admin-actions__kebab is shown in the same bottom-RIGHT
 *     corner slot BS-3 reserved (no layout shift vs the icon row).
 * Desktop (>480px) keeps the inline icons and never renders a kebab. */
.admin-table tbody td .admin-actions {
  justify-content: flex-end;
  flex-wrap: wrap;
  gap: var(--space-2);
  margin-top: 0;
}
/* The kebab is lifted out of flow to the card's top-right corner (below), so
   the trailing actions cell no longer reserves a bottom row — collapse its
   padding so it leaves no empty gap under the last meta line. Petřin pokyn
   2026-05-28 — the action entry belongs in the TOP hierarchy, not orphaned at
   the bottom (mockups 03/05 place the kebab top-right). */
.admin-table tbody td:has(> .admin-actions),
.admin-table tbody td.admin-actions {
  padding: 0;
}

/* Hide the inline action icons; the kebab stands in for them. (Read-only
 * — JS still finds them via querySelectorAll to mirror + .click().)
 * Two DOM shapes exist across renderers: most emit
 *   <td><div class="admin-actions">…icons…</div></td>  (innerHTML queues)
 * while Users / Crew emit
 *   <td class="admin-actions">…icons…</td>              (createElement)
 * — cover both so every tab's icons collapse behind the kebab. */
.admin-table tbody .admin-actions > .admin-actions__icon {
  display: none;
}

/* The single kebab — reuses the .admin-actions__icon look (no new button
 * variant) so it shares the row icon family. Pinned to the card's TOP-RIGHT
 * corner (the card <tr> is position:relative) so it reads as the row's action
 * entry point, mirroring mockups 03/05. */
.admin-actions__kebab {
  position: absolute;
  top: var(--space-2);
  right: var(--space-2);
  z-index: 1;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: var(--space-8);
  height: var(--space-8);
  padding: 0;
  background: transparent;
  border: 0;
  border-radius: var(--radius-sm);
  color: var(--text-muted);
  cursor: pointer;
}
/* Reserve right clearance on the first card line so its content never runs
   under the corner kebab (the reflow stacks all other lines left-aligned,
   below the kebab, so they keep full width). */
.admin-table tbody td:first-child {
  padding-right: calc(var(--space-8) + var(--space-2));
}
.admin-actions__kebab:active {
  background: rgba(var(--accent-turquoise-rgb), 0.10);
  color: var(--text-dark);
}

/* ── BS.5  Pagination (mobile restyle, SAME logic) ────────────────
 * adminPagination.js auto-mounts the SAME numbered pager (‹ 1 2 3 › +
 * Per-page select + "M–N of TOTAL") top + bottom of every table. The
 * desktop sheet already stacks it into a column at ≤640px; here we only
 * restyle for phone TOUCH — centre the nav, let the controls breathe,
 * and (in the pointer:coarse block below) lift the hit targets to 44px.
 * NO paging logic touched — purely the control chrome. */
.admin-pagination {
  /* The auto-mounted pager divs are siblings of the .admin-table card
     stack; give them the same outer rhythm so they don't hug the cards.
     Flex-wrap so the numbered nav takes its own row and "Per page" + the
     "1–N of TOTAL" info sit together on ONE line below it (Petřin pokyn
     2026-05-28 — info nemá viset na samostatném řádku). */
  display: flex;
  /* Override the ≤640px column-stack rule (style.css) — on a phone we want a
     wrapping ROW so "Per page" + "1–N of TOTAL" share one line. Without the
     explicit row direction the inherited flex-direction:column kept them on
     two lines. Petřin pokyn 2026-05-28 ("stále vidím ve dvou řádcích"). */
  flex-direction: row;
  flex-wrap: wrap;
  align-items: center;
  justify-content: center;
  margin: var(--space-2) 0;
  padding: var(--space-3);
  row-gap: var(--space-2);
  column-gap: var(--space-3);
  /* A faint surface so the pager reads as its own band between the list
     and the page edge (reuses the card surface tone — one system). */
  background: var(--cream-surface);
  border: 1px solid var(--border-on-light);
  border-radius: var(--radius-md);
}
.admin-pagination__nav {
  flex: 1 0 100%;                  /* numbered nav on its own row */
  justify-content: center;
  gap: var(--space-1);
}
/* Roomier number/arrow buttons for thumbs (desktop is 2rem square). */
.admin-pagination__btn {
  min-width: var(--space-8);
  height: var(--space-8);
}
/* "Per page [50]" + "1–N of TOTAL" flow together on the row below the nav. */
.admin-pagination__size,
.admin-pagination__info {
  flex: 0 1 auto;
  margin: 0;
  justify-content: center;
  text-align: center;
}
/* The "1–N of TOTAL" range carries the page math — on a phone the muted-grey
   0.85rem desktop styling made those numbers nearly disappear. Bump to the
   secondary-label size, semibold + bright text so the counts read (Petřin
   pokyn 2026-05-28). Mobile-only; desktop sidecar keeps its quiet treatment. */
.admin-pagination__info {
  font-size: var(--fs-sm);
  font-weight: var(--fw-semibold);
  color: var(--text-dark);
}

/* =============================================================
 * BS-6 — BACKSTAGE FULLSCREEN FORMS   (M12D-2, ADR-180)
 *
 * PLAIN-LANGUAGE: on a phone, the Backstage "create / edit" boxes
 * stop being little floating cards and take over the WHOLE screen,
 * the same way the fan app's "Add a Moment / Add a Concert" forms do.
 * A back-arrow sits top-left, the title is centred, the fields scroll
 * in the middle, and the main button is glued to the bottom edge above
 * the keyboard. Crew member: one screen (search a nickname, tick the
 * areas they can act on). Edit ban: one screen (duration + reason).
 *
 * TWO things happen here, both SURFACE-ONLY (no field / validation /
 * submit / step logic touched — ADR-180):
 *
 *  A. The four ADMIN CREATE forms (New concert / New online event /
 *     New landmark / New tour) reuse the SAME dialog shells the fan
 *     app uses (#event-create-dialog / #online-event-create-dialog /
 *     #poi-create-dialog / #tour-form-dialog). MO-5 already turns those
 *     fullscreen — so the CHROME is inherited for free. The only admin
 *     difference is the CTA TONE: on the fan map a new concert is teal
 *     and a new landmark is copper, but in Backstage every create lands
 *     directly (no fan review) so they all wear the IRIS/violet "create"
 *     tone. We override JUST the save-button colour, scoped to
 *     `.admin-body` so the fan map tones are untouched.
 *
 *  B. The Backstage `.frost-modal` forms (Add crew member, Add/Edit/Lift
 *     ban, legacy ban) are NOT in MO-5's list, so we give them the same
 *     fullscreen treatment MO-5 gives the fan frost-modals (suggest /
 *     delete): sticky glass title bar, scrolling body, sticky footer with
 *     a full-width primary above a full-width ghost Cancel. They carry
 *     `data-no-auto-close-button`, so the footer Cancel stays the exit
 *     (back / Esc also close via modalUtils; NO backdrop dismiss).
 *
 * CTA tone matrix (prompt-authoritative, overrides handoff gold):
 *   New concert / online / landmark / tour  → iris (= --accent-violet)
 *   Add crew member "Grant permissions"     → iris (grant = create-like)
 *   Edit ban "Save changes"                  → teal (= save)
 *   Add ban / Lift ban / legacy ban          → brick --danger (destructive)
 *
 * Tokenised throughout; raw px mapped to --fs-* / --space-* / --radius-*;
 * guarded ≤480px so desktop Backstage (≥720px) is byte-for-byte unchanged.
 * ============================================================= */

/* ── A. Admin create-form CTA tone → iris/violet ──────────────────
 * MO-5 paints these save buttons with the fan tones (teal / copper).
 * In Backstage they all read as "create" = iris. Scoped to .admin-body
 * so index.html (fan) keeps its per-category tones. Same gradient +
 * glow shape MO-5 uses, only the hue swaps to the violet family. */
.admin-body #event-create-dialog[open] .event-create__actions button[type="submit"],
.admin-body #poi-create-dialog[open] .event-create__actions button[type="submit"],
.admin-body #tour-form-dialog[open] .form-actions button[type="submit"] {
  background: linear-gradient(180deg, var(--accent-violet), var(--accent-violet-deep));
  box-shadow: 0 0 20px rgba(var(--accent-violet-rgb), 0.33), 0 6px 18px rgba(0, 0, 0, 0.40);
}

/* ── B. Backstage frost-modal forms → fullscreen ──────────────────
 * Shells in scope: Add crew · Add ban · Edit ban · Lift ban · legacy ban.
 * Mirror the MO-5 fullscreen frost-modal treatment (same gradients /
 * glass head + foot) on the admin dialog DOM. */
#admin-moderators-add-dialog[open],
#admin-users-add-dialog[open],
#admin-users-edit-dialog[open],
#admin-users-unban-dialog[open],
#admin-ban-dialog[open] {
  position: fixed;
  inset: 0;
  margin: 0;
  width: 100vw;
  max-width: 100vw;
  height: 100dvh;
  max-height: 100dvh;
  border: 0;
  border-radius: 0;
  padding: 0;
  overflow: hidden;
  color: var(--text-dark);
  /* Neutral cream→base gradient; tone variants below add the accent wash. */
  background: linear-gradient(180deg, var(--cream-surface) 0%, var(--cream-base) 100%);
}

/* Tone washes mirroring the save-button tone (iris create / danger ban). */
#admin-moderators-add-dialog[open] {
  background: radial-gradient(ellipse 80% 40% at 50% 0%, rgba(var(--accent-violet-rgb), 0.12) 0%, transparent 60%),
              linear-gradient(180deg, var(--cream-surface) 0%, var(--cream-base) 100%);
}
#admin-users-add-dialog[open],
#admin-users-unban-dialog[open],
#admin-ban-dialog[open] {
  background: radial-gradient(ellipse 80% 40% at 50% 0%, rgba(var(--danger-rgb), 0.12) 0%, transparent 60%),
              linear-gradient(180deg, var(--cream-surface) 0%, var(--cream-base) 100%);
}
/* Edit ban = save = teal wash. */
#admin-users-edit-dialog[open] {
  background: radial-gradient(ellipse 80% 40% at 50% 0%, rgba(var(--accent-turquoise-rgb), 0.10) 0%, transparent 60%),
              linear-gradient(180deg, var(--cream-surface) 0%, var(--cream-base) 100%);
}

/* Opaque-ink backdrop so nothing shows through the open/close frame. */
#admin-moderators-add-dialog[open]::backdrop,
#admin-users-add-dialog[open]::backdrop,
#admin-users-edit-dialog[open]::backdrop,
#admin-users-unban-dialog[open]::backdrop,
#admin-ban-dialog[open]::backdrop {
  background: var(--cream-base);
  -webkit-backdrop-filter: none;
  backdrop-filter: none;
}

/* The <form> becomes the flex column shell: title (sticky) · fields
 * (scroll) · footer (sticky). Overrides the desktop frost-modal `> form`
 * padding + centred card sizing. */
#admin-moderators-add-dialog[open] > form,
#admin-users-add-dialog[open] > form,
#admin-users-edit-dialog[open] > form,
#admin-users-unban-dialog[open] > form,
#admin-ban-dialog[open] > form {
  display: flex;
  flex-direction: column;
  height: 100dvh;
  max-height: 100dvh;
  min-height: 0;
  margin: 0;
  padding: 0;
  gap: 0;
  overflow: hidden;
}

/* ── Sticky glass title bar — centred italic display title. These forms
 * have no close button (data-no-auto-close-button), so the title centres
 * with symmetric side padding; back / Esc handle dismissal. */
#admin-moderators-add-dialog[open] .frost-modal__title,
#admin-users-add-dialog[open] .frost-modal__title,
#admin-users-edit-dialog[open] .frost-modal__title,
#admin-users-unban-dialog[open] .frost-modal__title,
#admin-ban-dialog[open] .frost-modal__title {
  position: sticky;
  top: 0;
  z-index: 10;
  flex: 0 0 auto;
  margin: 0;
  padding: calc(env(safe-area-inset-top) + var(--space-2)) var(--space-4) var(--space-3);
  background: linear-gradient(180deg, var(--glass-strong) 75%, transparent);
  -webkit-backdrop-filter: blur(14px);
  backdrop-filter: blur(14px);
  min-height: 56px;
  display: flex;
  align-items: center;
  justify-content: center;
  text-align: center;
  /* Canonical dialog title (1.55rem italic 700) — same standard as the
     fan auth + report headers (Petřin pokyn 2026-06-01). */
  font: italic 700 1.55rem var(--font-display);
  letter-spacing: 0.025em;
  color: var(--text-dark);
}

/* ── Scrolling body — the <form> itself scrolls; title is sticky-top,
 * footer sticky-bottom, fields flow between them. Give the field rows
 * horizontal gutters + comfortable vertical rhythm. */
#admin-moderators-add-dialog[open] > form,
#admin-users-add-dialog[open] > form,
#admin-users-edit-dialog[open] > form,
#admin-users-unban-dialog[open] > form,
#admin-ban-dialog[open] > form {
  overflow-y: auto;
  overflow-x: hidden;     /* keep wide rows (duration chips) inside the screen */
  -webkit-overflow-scrolling: touch;
}
#admin-moderators-add-dialog[open] > form > :not(.frost-modal__title):not(.form-actions),
#admin-users-add-dialog[open] > form > :not(.frost-modal__title):not(.form-actions),
#admin-users-edit-dialog[open] > form > :not(.frost-modal__title):not(.form-actions),
#admin-users-unban-dialog[open] > form > :not(.frost-modal__title):not(.form-actions),
#admin-ban-dialog[open] > form > :not(.frost-modal__title):not(.form-actions) {
  margin-left: var(--space-4);
  margin-right: var(--space-4);
  /* Let intrinsic-min-width children (fieldset, chip rows) shrink + wrap
     to the viewport instead of overflowing past the right edge. */
  min-width: 0;
  max-width: calc(100vw - 2 * var(--space-4));
}
/* The duration fieldset is a <fieldset>, which defaults to min-content
   width; force it to fill its gutter so the chips inside wrap. */
#admin-users-add-dialog[open] .admin-users__duration,
#admin-users-edit-dialog[open] .admin-users__duration {
  min-width: 0;
  width: 100%;
}
/* First field after the title bar gets top breathing room. */
#admin-moderators-add-dialog[open] .frost-modal__hint,
#admin-users-add-dialog[open] .frost-modal__hint,
#admin-users-edit-dialog[open] .frost-modal__hint,
#admin-users-unban-dialog[open] .frost-modal__hint,
#admin-ban-dialog[open] .frost-modal__hint {
  margin-top: var(--space-3);
}

/* Field/section vertical rhythm so the form breathes at fullscreen. */
#admin-moderators-add-dialog[open] .frost-modal__label,
#admin-moderators-add-dialog[open] .admin-moderators__perms,
#admin-users-add-dialog[open] .frost-modal__label,
#admin-users-add-dialog[open] .admin-users__duration,
#admin-users-edit-dialog[open] .frost-modal__label,
#admin-users-edit-dialog[open] .admin-users__duration,
#admin-users-unban-dialog[open] .frost-modal__label,
#admin-ban-dialog[open] .field,
#admin-ban-dialog[open] .frost-modal__label {
  margin-top: var(--space-3);
}

/* ── Sticky footer — full-width primary above full-width ghost Cancel,
 * pinned above the keyboard + the iOS gesture bar. The existing
 * .form-actions row becomes the bar. */
#admin-moderators-add-dialog[open] .form-actions,
#admin-users-add-dialog[open] .form-actions,
#admin-users-edit-dialog[open] .form-actions,
#admin-users-unban-dialog[open] .form-actions,
#admin-ban-dialog[open] .form-actions {
  position: sticky;
  bottom: 0;
  z-index: 10;
  flex: 0 0 auto;
  margin: 0;
  padding: var(--space-3) var(--space-4) calc(env(safe-area-inset-bottom) + var(--space-4));
  background: linear-gradient(180deg, transparent 0%, var(--glass-strong) 30%);
  -webkit-backdrop-filter: blur(14px);
  backdrop-filter: blur(14px);
  /* Primary on top, ghost Cancel below it (column-reverse keeps the
     submit visually first while Cancel stays last in the DOM). */
  display: flex;
  flex-direction: column-reverse;
  gap: var(--space-2);
}

/* Full-width primary submit — tone-coded per the matrix. Keeps its id /
 * class / handler; we only resize + repaint. */
#admin-moderators-add-dialog[open] .form-actions button[type="submit"],
#admin-users-add-dialog[open] .form-actions button[type="submit"],
#admin-users-edit-dialog[open] .form-actions button[type="submit"],
#admin-users-unban-dialog[open] .form-actions button[type="submit"],
#admin-ban-dialog[open] .form-actions button[type="submit"] {
  width: 100%;
  margin: 0;
  padding: var(--space-4);
  border-radius: var(--radius-md);
  border: 0;
  font: 700 var(--fs-md) var(--font-body);
  letter-spacing: 0.04em;
}
#admin-moderators-add-dialog[open] .form-actions button[type="submit"]:disabled,
#admin-users-add-dialog[open] .form-actions button[type="submit"]:disabled,
#admin-users-edit-dialog[open] .form-actions button[type="submit"]:disabled,
#admin-users-unban-dialog[open] .form-actions button[type="submit"]:disabled,
#admin-ban-dialog[open] .form-actions button[type="submit"]:disabled {
  opacity: 0.5;
  cursor: not-allowed;
}

/* Grant permissions = create-like → iris. */
#admin-moderators-add-dialog[open] .form-actions button[type="submit"] {
  color: var(--text-on-accent);
  background: linear-gradient(180deg, var(--accent-violet), var(--accent-violet-deep));
  box-shadow: 0 0 20px rgba(var(--accent-violet-rgb), 0.33), 0 6px 18px rgba(0, 0, 0, 0.40);
}
/* Edit ban "Save changes" = save → teal. */
#admin-users-edit-dialog[open] .form-actions button[type="submit"] {
  color: var(--text-on-accent);
  background: linear-gradient(180deg, var(--accent-turquoise), var(--accent-turquoise-deep));
  box-shadow: 0 0 20px rgba(var(--accent-turquoise-rgb), 0.33), 0 6px 18px rgba(0, 0, 0, 0.40);
}
/* Add ban / Lift ban / legacy ban = destructive → brick danger. */
#admin-users-add-dialog[open] .form-actions button[type="submit"],
#admin-users-unban-dialog[open] .form-actions button[type="submit"],
#admin-ban-dialog[open] .form-actions button[type="submit"] {
  color: var(--text-light);
  background: linear-gradient(180deg, var(--danger), var(--danger-strong, var(--danger)));
  box-shadow: 0 0 20px rgba(var(--danger-rgb), 0.33), 0 6px 18px rgba(0, 0, 0, 0.40);
}

/* Kept Cancel button — full-width ghost below the primary. */
#admin-moderators-add-dialog[open] .form-actions > button[type="button"],
#admin-users-add-dialog[open] .form-actions > button[type="button"],
#admin-users-edit-dialog[open] .form-actions > button[type="button"],
#admin-users-unban-dialog[open] .form-actions > button[type="button"],
#admin-ban-dialog[open] .form-actions > button[type="button"] {
  width: 100%;
  margin: 0;
  padding: var(--space-3);
  border-radius: var(--radius-md);
}

/* Crew "Add crew member" permission checkboxes — the inherited grid wraps
   with no gap on a phone ("ConcertsOnline events"). Stack each permission
   as its own full-width row: checkbox + label with breathing room. */
#admin-moderators-add-dialog[open] .admin-moderators__perms {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
}
#admin-moderators-add-dialog[open] .admin-moderators__perm-row {
  display: flex;
  align-items: flex-start;
  gap: var(--space-2);
  width: 100%;
}
#admin-moderators-add-dialog[open] .admin-moderators__perm-row > input[type="checkbox"] {
  flex: 0 0 auto;
  margin-top: 2px;
}
#admin-moderators-add-dialog[open] .admin-moderators__perm-label {
  flex: 1 1 auto;
  min-width: 0;
}

/* =============================================================
 * BS-7 — BACKSTAGE MOBILE BULK SELECTION   (M12D-2, ADR-180)
 *
 * PLAIN-LANGUAGE: on the Reports + Test Data tabs a moderator can pick
 * several cards at once and act on all of them. Press-and-hold a card →
 * "selection mode": each card shows a round tick, its ⋮ menu hides, and
 * tapping a card adds/removes it. A bar slides up at the bottom with the
 * count + the same bulk buttons the desktop has + an × to leave.
 *
 * SURFACE-ONLY: the markup is the existing rows (each already has a real
 * row checkbox) + the existing desktop bulk buttons. adminMobileBulk.js
 * drives those (sets checkbox.checked + dispatches change; clicks the
 * desktop bulk buttons). This block is presentation only — it dresses the
 * existing checkbox cell as a tick, hides the kebab during selection, and
 * styles the bar. No logic / counting / action change.
 *
 * SCOPE: only the table flagged `.bs-bulk-table` by the helper (Reports or
 * Test Data) reacts — the other table (a hidden tab) stays inert. Already
 * inside @media (max-width:480px), so desktop is untouched.
 * ============================================================= */

/* The desktop bulk toolbar (Dismiss / Delete · Convert / Delete) is the
 * DESKTOP affordance. On a phone the long-press bar IS the bulk affordance,
 * so hide the toolbar pair to avoid two competing controls — the buttons
 * STAY in the DOM (display:none keeps them clickable) so adminMobileBulk.js
 * can still mirror their label + disabled state and trigger their .click().
 * Same philosophy as BS-4 hiding the inline icons behind the kebab. */
.admin-reports__batch-actions,
.admin-test-data__batch-actions {
  display: none;
}

/* The leading checkbox cell is meaningless in the card layout outside
 * selection mode — collapse it so a normal card has no stray checkbox line
 * (it stays in the DOM; the helper reads + toggles it). The BS-3 rule
 * `.admin-table tbody td` sets every cell to display:block, so match its
 * specificity to win. */
.admin-table tbody td.admin-reports__col-check,
.admin-table tbody td.admin-test-data__col-check {
  display: none;
}

/* In selection mode, reveal the checkbox cell as a round tick in the card's
 * top-right corner (where the kebab sits otherwise). We hide the native
 * checkbox and paint the tick on its cell so the affordance reads as the
 * Noir selection control — no new variant, reuses the turquoise selected
 * tone the desktop already uses for the row tint. */
.bs-selecting .bs-bulk-table tbody tr .admin-reports__col-check,
.bs-selecting .bs-bulk-table tbody tr .admin-test-data__col-check {
  position: absolute;
  top: var(--space-3);
  right: var(--space-3);
  width: var(--space-6);
  height: var(--space-6);
  margin: 0;
  padding: 0;
  border-radius: 999px;
  border: var(--space-px) solid var(--hairline-dark);
  background: transparent;
  display: grid;
  place-items: center;
  color: transparent;
  pointer-events: none; /* the whole card body is the tap target */
}

/* Hide the real checkbox — the cell itself is the tick. */
.bs-selecting .bs-bulk-table tbody tr .admin-reports__col-check input[type="checkbox"],
.bs-selecting .bs-bulk-table tbody tr .admin-test-data__col-check input[type="checkbox"] {
  display: none;
}

/* The check glyph, injected via ::after; shown only on selected rows. */
.bs-selecting .bs-bulk-table tbody tr .admin-reports__col-check::after,
.bs-selecting .bs-bulk-table tbody tr .admin-test-data__col-check::after {
  content: "";
  width: 60%;
  height: 60%;
  background: currentColor;
  -webkit-mask: var(--bs-check-mask) center / contain no-repeat;
          mask: var(--bs-check-mask) center / contain no-repeat;
}
.admin-body {
  /* Tick mark drawn as a masked SVG so the colour follows currentColor. */
  --bs-check-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='3' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M5 13l4 4L19 7'/%3E%3C/svg%3E");
}

/* Selected row → filled turquoise tick + the existing row tint (the desktop
 * --selected rule already paints .admin-reports__row--selected /
 * .admin-test-data__row--selected with --accent-turquoise-tint; we add the
 * tick fill + a soft ring so the picked card reads at a glance). */
.bs-selecting .bs-bulk-table tbody tr.admin-reports__row--selected .admin-reports__col-check,
.bs-selecting .bs-bulk-table tbody tr.admin-test-data__row--selected .admin-test-data__col-check {
  background: var(--accent-turquoise);
  border-color: var(--accent-turquoise);
  color: var(--surface-ink);
}
.bs-selecting .bs-bulk-table tbody tr.admin-reports__row--selected,
.bs-selecting .bs-bulk-table tbody tr.admin-test-data__row--selected {
  box-shadow: var(--shadow-card), 0 0 0 1px rgba(var(--accent-turquoise-rgb), 0.45);
}

/* During selection mode the per-row kebab is hidden (the card body IS the
 * toggle), and the title link is made inert so a card-body tap selects
 * instead of navigating. Scoped to the bulk table only. */
.bs-selecting .bs-bulk-table tbody tr .admin-actions__kebab {
  display: none;
}
.bs-selecting .bs-bulk-table tbody tr .admin-row__title-link {
  pointer-events: none;
}
/* Give the card a touch more right padding so the tick never overlaps text. */
.bs-selecting .bs-bulk-table tbody tr:not(.admin-mod-section):not(.admin-table__spacer-row):not(.admin-table__empty-row) {
  padding-right: var(--space-8);
  cursor: pointer;
}

/* ── BS-7  Sticky bulk-action bar ─────────────────────────────────
 * Glass bar pinned to the bottom edge above the safe-area inset. Count on
 * the left, mirrored primary + danger actions, exit × on the right. Layer
 * on the chrome/tab-bar plane so it sits above the cards. */
.admin-bulk-bar {
  position: fixed;
  left: 0;
  right: 0;
  bottom: 0;
  z-index: var(--m13-z-tab-bar);
  display: flex;
  align-items: center;
  gap: var(--space-2);
  padding: var(--space-3) var(--space-3)
           calc(env(safe-area-inset-bottom) + var(--space-3));
  background: linear-gradient(180deg, transparent, var(--cream-base) 28%);
  border-top: var(--space-px) solid var(--hairline-dark);
  -webkit-backdrop-filter: blur(18px);
          backdrop-filter: blur(18px);
}

.admin-bulk-bar__count {
  flex-shrink: 0;
  font-size: var(--fs-xs);
  letter-spacing: 0.04em;
  color: var(--text-muted);
}
.admin-bulk-bar__count strong {
  color: var(--accent-turquoise);
  font-weight: 700;
}

.admin-bulk-bar__actions {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  margin-left: auto;
}

/* Mirrored bulk buttons — pill shape, 44px tall touch targets. Primary =
 * teal save fill (reuses the turquoise gradient the fullscreen save uses);
 * danger = brick --danger outline (destructive tone per ADR-169). No new
 * button variant — same tone vocabulary as the rest of the mobile layer. */
.admin-bulk-bar__btn {
  min-height: 44px;   /* WCAG touch-target floor (= the Z-section convention) */
  padding: 0 var(--space-4);
  border-radius: 999px;
  border: var(--space-px) solid transparent;
  font-size: var(--fs-sm);
  font-weight: 600;
  cursor: pointer;
}
.admin-bulk-bar__btn:disabled {
  opacity: 0.5;
  cursor: default;
}
.admin-bulk-bar__btn--primary {
  color: var(--text-on-accent);
  background: linear-gradient(180deg, var(--accent-turquoise), var(--accent-turquoise-deep));
  box-shadow: 0 0 16px rgba(var(--accent-turquoise-rgb), 0.30);
}
.admin-bulk-bar__btn--danger {
  color: var(--danger-text);
  background: rgba(var(--danger-rgb), 0.10);
  border-color: rgba(var(--danger-rgb), 0.40);
}

.admin-bulk-bar__exit {
  flex-shrink: 0;
  width: 44px;    /* WCAG touch-target floor for the primary exit control */
  height: 44px;
  display: grid;
  place-items: center;
  border-radius: 999px;
  border: var(--space-px) solid var(--hairline-dark);
  background: var(--hairline-dark-lo);
  color: var(--text-muted);
  cursor: pointer;
}

}  /* end nested @media (max-width: 480px) — admin/backstage stays phone-only */

}  /* end @media (max-width: 767px) — M13.X-CHROME-REFLOW unified mobile chrome */

/* =============================================================
 * Z. Tap-target floor — @media (pointer: coarse)
 *
 * Independent of the width guard: a coarse pointer (touch) needs
 * 44px targets whatever the width. Scoped to .fm-* only so it
 * never touches desktop primitives. (style.css already has its
 * own pointer:coarse block for .icon-btn — no overlap.)
 * ============================================================= */
@media (pointer: coarse) {
  .fm-side-btn,
  .fm-mobile-header__btn,
  .fm-tab,
  /* .fm-rsvp__btn / .fm-rsvp__save intentionally EXCLUDED — M12.X-RSVP-UNIFY
     pins them to the 34px desktop-popup reference height (Petřin pokyn
     2026-05-30), which the 44px coarse floor would override on touch. */
  .fm-sheet__close,
  .fm-form-fullscreen__back,
  .fm-quick-menu__item,
  .fm-action-sheet__item,
  .fm-peek-header__kebab,
  .fm-moment-row__more,
  .fm-icon-btn {
    min-width: 44px;
    min-height: 44px;
  }
}

/* M12D mobile audit 2026-05-30 — the moderator delete-reason dialog packs 5
 * reason radios into a centred confirm card; the desktop 0.35rem row padding
 * left ~29px rows, below the 44px touch floor and visibly tighter than the
 * fullscreen report dialog's airy reason list. Give touch users roomier,
 * P-030-compliant rows in BOTH portrait and landscape (un-width-gated so a
 * phone held sideways, ≥481px, is covered too). Desktop mouse keeps the
 * compact canonical spacing. */
@media (pointer: coarse) {
  .mod-delete-reason-dialog__option {
    min-height: 44px;
    padding: 0.55rem 0.5rem;
    gap: 0.7rem;
  }
}

/* BS-1 — Backstage per-row action icons need the 44px touch floor on
 * real touch devices (the desktop .admin-actions__icon is ~32px). Scoped
 * ≤480px so it only applies in the mobile card layout; desktop admin is
 * untouched. (BS-2 collapses these behind one kebab; this keeps them
 * tappable in the interim card layout.) */
@media (pointer: coarse) and (max-width: 480px) {
  .admin-actions__icon,
  .admin-actions__kebab,
  .audit-detail-btn,
  .admin-tab,
  .admin-header__back,
  .admin-row__title-link,
  .admin-pagination__btn,
  .admin-pagination__size-select {
    min-width: 44px;
    min-height: 44px;
  }
}

/* Desktop guard — if a phone-width session is resized up across 480px, the
 * injected kebab stays in the DOM (adminMobileActions.js leaves it); hide it
 * and restore the inline icons so desktop admin reads exactly as before. */
@media (min-width: 481px) {
  .admin-actions__kebab { display: none; }
  /* M12D-3 S1.3 — the tab line icons belong to the ≤480px bottom tab bar
   * only. In the legacy 481–767px pill band the tabs stay label-only, so
   * the injected SVG must not render there. (Hidden entirely on desktop
   * already, since .mobile-pill-bar is display:none ≥768px.) */
  .mobile-pill-bar .hero-tab .hero-tab__icon { display: none; }
}

/* MO-2 — touch floor for the panel header close + FAB-menu rows on real
 * touch devices (the round × is 34px visually; expand its hit area). */
@media (pointer: coarse) and (max-width: 480px) {
  .fm-tab-panel__close,
  #fm-panel-close {
    min-width: 44px;
    min-height: 44px;
  }
  /* MO-5 — fullscreen-form back arrow (the reskinned close button) +
   * the sticky Save / Cancel buttons need the 44px touch floor. */
  #pin-form-dialog[open] .pin-form__close,
  #event-create-dialog[open] .dialog-close,
  #online-event-create-dialog[open] .dialog-close,
  #poi-create-dialog[open] .dialog-close,
  #report-form-dialog[open] .dialog-close,
  /* M13 review MOB-001 — the Community Hub back arrow missed this floor
   * (40px from the shared back-arrow reskin block above). */
  #community-hub-dialog[open] .dialog-close,
  #pin-form-dialog[open] #pin-form-submit,
  #event-create-dialog[open] .event-create__actions button,
  #online-event-create-dialog[open] .event-create__actions button,
  #poi-create-dialog[open] .event-create__actions button,
  #report-form-dialog[open] .form-actions button,
  #change-suggestion-dialog[open] .form-actions button,
  #deletion-request-dialog[open] .form-actions button {
    min-width: 44px;
    min-height: 44px;
  }
  /* MO-6 — fullscreen auth/settings back arrow + sticky-bar buttons. */
  #auth-login-dialog[open] > .dialog-close,
  #auth-register-dialog[open] > .dialog-close,
  #auth-verify-dialog[open] > .dialog-close,
  #auth-forgot-email-dialog[open] > .dialog-close,
  #auth-forgot-reset-dialog[open] > .dialog-close,
  #auth-federated-consent-dialog[open] > .dialog-close,
  #auth-delete-confirm-dialog[open] > .dialog-close,
  #auth-mfa-setup-dialog[open] > .dialog-close,
  #auth-mfa-disable-dialog[open] > .dialog-close,
  #auth-mfa-challenge-dialog[open] > .dialog-close,
  #auth-profile-dialog[open] > .dialog-close,
  #auth-preferences-dialog[open] > .dialog-close,
  #auth-security-dialog[open] > .dialog-close,
  #auth-data-dialog[open] > .dialog-close,
  #auth-login-dialog[open] .form-actions button,
  #auth-register-dialog[open] .form-actions button,
  #auth-verify-dialog[open] .form-actions button,
  #auth-forgot-email-dialog[open] .form-actions button,
  #auth-forgot-reset-dialog[open] .form-actions button,
  #auth-federated-consent-dialog[open] .form-actions button,
  #auth-delete-confirm-dialog[open] .form-actions button,
  #auth-mfa-setup-dialog[open] .form-actions button,
  #auth-mfa-disable-dialog[open] .form-actions button,
  #auth-mfa-challenge-dialog[open] .form-actions button,
  #auth-profile-dialog[open] .form-actions button,
  #auth-preferences-dialog[open] .form-actions button,
  #auth-security-dialog[open] .form-actions button,
  #auth-data-dialog[open] .form-actions button {
    min-width: 44px;
    min-height: 44px;
  }
}

/* MO-7 (M15 review AI-002/003/004) — touch floor for the M15 tour-propose
 * controls + Places spider menu. The teal circular icon buttons (collapse
 * chevron / description ↻, 30px visual) keep their size; an invisible
 * ::before overlay grows the hit area to 44px (none of these buttons use
 * pseudo-elements otherwise). The auto-match dismiss × and the spider menu
 * rows grow their real box instead (ghost button / full-width row — a
 * bigger box is the intended look on touch). */
@media (pointer: coarse) and (max-width: 767px) {
  .venue-card__map-toggle,
  .event-create__tour-new-collapse,
  .event-create__tour-desc-refresh,
  .event-create__desc-refresh,
  .poi-desc-refresh {
    position: relative;
  }
  .venue-card__map-toggle::before,
  .event-create__tour-new-collapse::before,
  .event-create__tour-desc-refresh::before,
  .event-create__desc-refresh::before,
  .poi-desc-refresh::before {
    content: "";
    position: absolute;
    inset: -7px;
    border-radius: 50%;
  }
  .event-create__tour-suggest-dismiss {
    min-width: 44px;
    min-height: 44px;
    display: inline-flex;
    align-items: center;
    justify-content: center;
  }
  .spider-menu__item {
    min-height: 44px;
  }
}

/* MO-8 (M18 review AI-005, M-UX-M18-001/002/003/004/005/007) — touch-target
 * hit batch. IRON RULE: the visuals are Petra's deliberate choices (rail
 * tiles 34px, satellite pills ~28px, close discs 30-32px) and stay
 * pixel-identical — only the INVISIBLE hit zone grows, via the MO-7
 * overlay pattern (::before/::after + position:relative). The two
 * review-sanctioned visible exceptions are flagged inline. */

/* M-UX-M18-001 — rail tiles (JOIN / LANDMARKS / HUB): 34px tiles, 5px
 * apart, in the 46px capsule. Horizontal -6px reaches the capsule edge
 * (6px capsule padding each side = full 46px-wide hit); vertical -2px is
 * the honest cap — the 5px tile gap leaves the neighbouring zones 1px
 * apart, so a thumb can never trip two tiles at once. The rail-tile skin
 * only exists ≤767px, hence the width guard. */
@media (pointer: coarse) and (max-width: 767px) {
  #map-action-join,
  #map-action-community,
  .spider-menu .spider-menu__button {
    position: relative;
  }
  #map-action-join::before,
  #map-action-community::before,
  .spider-menu .spider-menu__button::before {
    content: "";
    position: absolute;
    inset: -2px -6px;
  }

  /* M-UX-M18-007 — header LOCATE: hit box 32 → 44px wide. Its box used to
   * OVERLAP the lupa's 48px circle by 6px (a tap on the lupa's right rim
   * could fire geolocation); the new left edge sits exactly ON the lupa's
   * edge — touching, not overlapping. The glyph must not move a pixel:
   * padding-right shrinks the grid content area to exactly the 20px glyph,
   * pinning it to the same screen x as before while the real (invisible —
   * transparent, borderless) box grows to the right over empty header. */
  .map-actions__slot--locate {
    left: calc(14px + var(--m13-header-pill-h));
  }
  .map-actions__slot--locate .map-actions__btn--locate {
    width: 44px;
    padding-right: 24px;
  }
}

/* The satellite pills + the give-stars / Leave-gathering dialogs exist at
 * every width, so the rest of MO-8 is coarse-only (no width guard), like
 * the Z tap-floor block above. */
@media (pointer: coarse) {
  /* M-UX-M18-003 — satellite pills (~28px tall; "Back to map" is the ONLY
   * mobile route back to the overview, the news pill the main M18
   * interaction). An invisible ::after lifts the effective hit to ~40px.
   * Horizontal inset stays 0 — row neighbours must not steal each other's
   * taps. Neither ::before nor ::after is used by the pill family (the
   * status dot is a real <span>), so the overlay is collision-free.
   * The 0.45 → 0.55rem row gap is sanctioned visible change #1 (review
   * 2026-06-11): with two wrapped rows on drop day the ±6px zones need
   * ≥8.8px between rows. */
  .hero-badges {
    gap: 0.55rem;
  }
  .online-live-badge {
    position: relative;
  }
  /* NOT on the news pill wrapper: a wrapper ::after (pseudo = painted
   * after all children) would sit OVER the inner dismiss ×, re-targeting
   * every × tap to the wrapper = fly-to instead of dismiss. The news pill
   * grows via its inner fly button below instead. */
  .online-live-badge:not(.map-news-badge)::after {
    content: "";
    position: absolute;
    inset: -6px 0;
  }
  .map-news-badge__main {
    position: relative;
  }
  .map-news-badge__main::after {
    content: "";
    position: absolute;
    /* Reach 6px past the PILL edge — the wrapper's 0.34rem vertical
     * padding sits between this inner button and the pill border. */
    inset: calc(-6px - 0.34rem) 0;
  }

  /* M-UX-M18-002 — news pill dismiss ×: hit ~23×27 → ~42×35px. Same trick
   * the base rule already uses, turned up: padding grows the zone,
   * negative margins hand the space straight back, so the pill's size and
   * the glyph's position are untouched. margin-left flips to -0.4rem
   * (= the pill's flex gap): the hit boundary now sits at the fly
   * button's edge, so a mistap on the seam DISMISSES instead of flying
   * the map across the world. The divider can NOT stay a border-left —
   * a border spans the padded box and would visibly stretch into a ~35px
   * line poking out of the 28px pill — so a fixed-size ::before repaints
   * it at the exact same spot (x = 0.55rem right of the fly button),
   * same 1px × 1.7rem geometry, same colour. */
  .map-news-badge__dismiss {
    position: relative;
    border-left: 0;
    margin: -0.65rem -1.1rem -0.65rem -0.4rem;
    padding: 0.65rem 1.1rem 0.65rem calc(0.95rem + 1px);
  }
  .map-news-badge__dismiss::before {
    content: "";
    position: absolute;
    left: 0.55rem;
    top: 50%;
    transform: translateY(-50%);
    width: 1px;
    /* = the old border-box height: 0.9rem glyph line + 2 × 0.4rem padding. */
    height: 1.7rem;
    background: rgba(var(--live-rgb), 0.25);
  }

  /* M-UX-M18-004 — give-stars dialog: the ± steppers (34px) and the
   * close ⊗ (30px, already position:absolute — must NOT be re-positioned)
   * keep their look; ::before lifts the hit to 44/40px. The Echo/Rate
   * action pair are full-width surfaces, so they grow their REAL box to
   * the 44px floor per P-041 amendment — sanctioned visible change #2. */
  .rating-step {
    position: relative;
  }
  .rating-step::before {
    content: "";
    position: absolute;
    inset: -5px;
  }
  .rating-dialog__close::before {
    content: "";
    position: absolute;
    inset: -5px;
    border-radius: 50%;
  }
  .rating-dialog__echo,
  .rating-dialog__rate {
    min-height: 44px;
  }

  /* M-UX-M18-005 — Leave/Delete-gathering dialog close × (32px circle,
   * position:absolute) → overlay only, NOT min-width/height: growing the
   * real box would shift the absolutely anchored circle. 32+14 = 46px hit. */
  .delete-kotva-modal__close::before {
    content: "";
    position: absolute;
    inset: -7px;
    border-radius: 50%;
  }
}

/* ==========================================================================
   M12.X-MAP-ACTIONS round 2 — neutralise the desktop action rail layout
   on phones so the M14 vertical rail can take over. The rail's
   .spider-menu (mounted into .map-actions__slot--places by main.js) MUST
   still render on mobile — it is a tile of the M14 vertical rail
   (m14-glass-rail.md). Earlier iteration used `#map-actions
   { display: none }` which dropped the whole subtree from the render
   tree — Petřin smoke 2026-05-28 caught the missing tile on phone width.
   Fix: `display: contents` on the rail + its slots so the wrapper boxes
   vanish from layout flow but the button children stay alive. The mobile
   `position: fixed` rules elsewhere in this file then re-anchor each onto
   the M14 rail. (Petřin pokyn 2026-05-28; Songs tile removed by
   M13.X-SONGS-TAB ADR-237 — the song world lives in the panel tabs now.)
   ========================================================================== */
/* M13.X-CHROME-REFLOW — lifted 480 → 767 in lock-step with the main mobile
   block: this is where the desktop action rail is neutralised (display:contents)
   and the JOIN/LOCATE tiles re-anchor onto the fixed M14 vertical rail. Must
   fire across the whole mobile range or the desktop rail's pixel-math position
   would resurface at 481–767px. */
@media (max-width: 767px) {
  /* M13.X-PEBBLES (ADR-247) — the old display:contents neutraliser + fixed
   * per-tile `top:` steps are gone: #map-actions IS the vertical capsule
   * (flex column, see section C in the main mobile block), so the JOIN and
   * LOCATE buttons below only need their square-tile SKIN. Rail order:
   * LOCATE · JOIN · Places · Legend (actionRail.js RAIL_ORDER). */

  /* Tile — JOIN. Violet tone (Petřin pokyn 2026-06-10; was magenta) — JOIN now
   * carries the open-gathering identity (designer marker). Same square skin. */
  #map-action-join.map-actions__btn--join {
    width: var(--m14-rail-tile);
    height: var(--m14-rail-tile);
    min-height: var(--m14-rail-tile);
    box-sizing: border-box;
    padding: 0;
    border-radius: var(--m14-rail-tile-radius);
    background:
      radial-gradient(circle at 35% 30%,
        rgba(var(--accent-violet-rgb), 0.30),
        rgba(var(--accent-violet-rgb), 0.08) 85%);
    border: 1.5px solid rgba(var(--accent-violet-rgb), 0.70);
    color: var(--accent-violet);
    display: grid;
    place-items: center;
    box-shadow: inset 0 1px 0 rgba(238, 240, 250, 0.10);
  }

  /* Tile — COMMUNITY (HUB). Teal tone (Petřin pokyn 2026-06-10; was violet
   * per ADR-247 R4). Bottom of the rail, same square skin family. */
  #map-action-community.map-actions__btn--community {
    width: var(--m14-rail-tile);
    height: var(--m14-rail-tile);
    min-height: var(--m14-rail-tile);
    box-sizing: border-box;
    padding: 0;
    border-radius: var(--m14-rail-tile-radius);
    background:
      radial-gradient(circle at 35% 30%,
        rgba(var(--accent-turquoise-rgb), 0.30),
        rgba(var(--accent-turquoise-rgb), 0.08) 85%);
    border: 1.5px solid rgba(var(--accent-turquoise-rgb), 0.70);
    color: var(--accent-turquoise-bright, var(--accent-turquoise));
    display: grid;
    place-items: center;
    box-shadow: inset 0 1px 0 rgba(238, 240, 250, 0.10);
  }

  /* (LOCATE tile skin removed 2026-06-10 — LOCATE left the rail for the
     header beside the lupa. #map-locate in the header section styles it as
     a bare teal glyph with no tile / border, aligned with the lupa.) */

  /* Glyphs on the rail tiles — match rail glyph scale + drop-shadow. */
  #map-action-community.map-actions__btn--community svg {
    width: 20px;                   /* M-rail-shrink 2026-06-10 — match the 20px Places glyph in the smaller 34px tile */
    height: 20px;
    filter: drop-shadow(0 1px 2px rgba(0, 0, 0, 0.80));
  }
  /* JOIN glyph bigger (Petřin pokyn 2026-06-10 "nevýrazné") — the designer's
     ringed marker needs more room than the simple Places/HUB glyphs to read.
     translateY nudges it down (Petřin pokyn "trochu posuň dolů"): the marker's
     mass sits in the upper rings, so centred-in-box it reads top-heavy. */
  #map-action-join.map-actions__btn--join svg {
    width: 24px;
    height: 24px;
    transform: translateY(2px);
    filter: drop-shadow(0 1px 2px rgba(0, 0, 0, 0.80));
  }

  /* Pressed feedback — depress 1px, like the sibling tiles. */
  #map-action-join.map-actions__btn--join:active,
  #map-action-community.map-actions__btn--community:active {
    transform: translateY(1px);
  }

  /* JOIN active (filter on) — resting look + thicker magenta ring + bold glow
     (Petřino rozhodnutí 2026-06-03; mirrors the desktop pebble). */
  #map-action-join.map-actions__btn--join.map-actions__btn--active {
    border-color: var(--accent-mine);
    box-shadow:
      0 0 0 2.5px var(--accent-mine),
      0 0 18px rgba(var(--accent-mine-rgb), 0.85),
      0 0 34px rgba(var(--accent-mine-rgb), 0.40),
      inset 0 1px 0 rgba(238, 240, 250, 0.10);
  }

  /* Toasts must clear the bottom tab bar (M13.X-JOIN-DISCOVERY — the JOIN
     hint toast collided with the bar; desktop keeps its 1.25rem offset). */
  .toast-stack {
    bottom: calc(var(--m13-tab-bar-h) + env(safe-area-inset-bottom, 0px) + 12px);
  }
}
