/* ==========================================================================
   Mobile Foundation — FFA Mobile Redesign
   Applies ONLY at <768px. Desktop and tablet layouts unchanged.
   ========================================================================== */

/* ==========================================================================
   UNIVERSAL MOBILE MODAL CHROME CONTRACT
   (reflective-swimming-ripple S1 D1-A, 2026-05-11 — Issue #1)
   --------------------------------------------------------------------------
   Every FFA mobile modal — Library, Media, Family, Fergi AI, future bio
   popup, future surfaces — sits inside a 3-layer chrome envelope:

     ┌─────────────────────────────────────────────┐
     │  TOP BAND  (.top-nav, height = --mobile-      │  ← global, always
     │             top-bar-total-height)             │    visible
     │  · hamburger left                             │
     │  · modal title centered (.mobile-view-title)  │
     │  · Fergi AI launcher right                    │
     ├─────────────────────────────────────────────┤
     │                                               │
     │  MODAL CONTENT  (full-bleed vertically        │
     │                  between TOP and BOTTOM,      │
     │                  100% width minus safe-area)  │
     │                                               │
     ├─────────────────────────────────────────────┤
     │  BOTTOM BAND  (.mobile-tab-bar, height =      │  ← global, always
     │                --mobile-tab-bar-height +      │    visible (except
     │                safe-area-bottom)              │    during full-screen
     │  · color-matched to active modal palette via  │    presentation
     │    body[data-modal-palette=<view>]            │    playback)
     └─────────────────────────────────────────────┘

   Implementation patterns (both valid):

     1. IN-FLOW modal (e.g. .mobile-library, .fnav-root):
        Mounts inside .main-content. Top inherits .main-content's
        `margin-top: var(--mobile-top-bar-total-height)`. Bottom uses
        `padding-bottom: var(--mobile-modal-bottom-reservation)` (defined
        in mobile-tab-bar.css) so last content row clears the tab bar.

     2. FIXED-OVERLAY modal (e.g. .unified-ai-modal.active,
        .browse-media-modal.active):
        Uses position: fixed with
          top:    calc(var(--mobile-top-bar-height) + var(--mobile-safe-area-top))
          bottom: calc(var(--mobile-tab-bar-height) + var(--mobile-safe-area-bottom))
        Both anchors share the same vars so the chrome remains visually
        consistent even when one modal stacks above another.

   Bottom-band palette match (D1-D, 2026-05-11): when a tab/modal becomes
   active, tab-bar.js sets `body[data-modal-palette="<tab-id>"]`. CSS in
   mobile-tab-bar.css uses that selector to tint the tab-bar background +
   accent vars (--ffa-tier-1-base / --ffa-tier-2-base / Family substrate)
   so the bottom band reads as part of the active modal's surface.

   New modal-class surfaces opt into the contract by either:
     - mounting inside .main-content (in-flow); OR
     - adding `.ffa-mobile-modal-chrome` to their root (fixed-overlay).
   ========================================================================== */

@media (max-width: 767px) {

  :root {
    --mobile-top-bar-height: 56px;
    /* realizing-mobile-chrome-contract S1 D3 (2026-05-14) — Iron-Law bottom-panel
       tightening. Joe iPhone /listen Issue #1 (universal across modals):
       *"that [bottom Mobile Control Panel] at the very bottom of the display
       in one thin panel but it is still wasting at least its own height in
       vertical space that could be devoted to the main modal area."* Pre-fix
       value 56px contained ~5px slack above + below the 51px icon+label
       content within each tab item; reducing the var to 48px tightens the
       band to ~the minimum that still keeps tap targets ≥44pt iOS HIG
       compliant (24px icon + 3px gap + 14px label + 6+4 padding = 51px > 48px
       overflow protection is handled by the icon row's own flex-center, so
       the visual band shrinks to the content's own height + breathing room.
       Companion edit at mobile-tab-bar.css:24 keeps --mobile-control-panel-height
       in lockstep so modal padding-bottom reservation tracks the actual bar
       height (no double-reservation drift). */
    --mobile-tab-bar-height: 48px;
    --mobile-safe-area-top: env(safe-area-inset-top, 0px);
    --mobile-safe-area-bottom: env(safe-area-inset-bottom, 0px);
    --mobile-safe-area-left: env(safe-area-inset-left, 0px);
    --mobile-safe-area-right: env(safe-area-inset-right, 0px);
    /* Single source of truth for the visible mobile header band (56px) PLUS the
       iPhone safe-area inset (notch/Dynamic Island, ~47–59px on iPhone 13+).
       Used by .top-nav height, .main-content margin-top, .modal-overlay top
       across every mobile-aware stylesheet. binary-hatching-pearl D1 Iron-Law
       fix: eliminates the stale `var(--nav-height)` (60px, no safe-area)
       overrides in stories.css that hid the hamburger behind the Dynamic
       Island on iPhone Safari. */
    --mobile-top-bar-total-height: calc(var(--mobile-top-bar-height) + var(--mobile-safe-area-top));
    /* Universal chrome-contract bottom anchor — the height the bar plus
       safe-area-bottom inset occupies. Mirrors --mobile-control-panel-height
       in mobile-tab-bar.css (kept in sync). Fixed-overlay modals use this for
       their `bottom:` anchor; flow-positioned modals already get bottom
       reservation via --mobile-modal-bottom-reservation. */
    --mobile-tab-bar-total-height: calc(var(--mobile-tab-bar-height) + var(--mobile-safe-area-bottom));
  }

  /* ── Chrome-contract utility class (reflective-swimming-ripple S1 D1-A) ──
     Fixed-overlay modals opt-in by adding `.ffa-mobile-modal-chrome` to their
     root. Sets the top + bottom anchors to the universal chrome bands so the
     modal sits flush between the global top-nav and the global tab-bar with
     no gap and no overlap. Width is full-bleed minus safe-area-inset-l/r.
     The class does NOT set z-index — modal callers own z-index per their
     stacking-context rules (see modal-mutual-exclusion contract).

     Existing modals that already match the contract via inline CSS
     (.unified-ai-modal.active, .browse-media-modal.active) need not adopt
     the class — the class is a forward-compatible shortcut for future
     modals so we don't repeat the calc() math 5 more times. */
  .ffa-mobile-modal-chrome {
    position: fixed !important;
    top: var(--mobile-top-bar-total-height) !important;
    bottom: var(--mobile-tab-bar-total-height) !important;
    left: 0 !important;
    right: 0 !important;
    width: 100% !important;
    max-width: 100% !important;
    padding-left: var(--mobile-safe-area-left);
    padding-right: var(--mobile-safe-area-right);
    box-sizing: border-box;
    overflow-y: auto;
    -webkit-overflow-scrolling: touch;
    /* The chrome envelope is self-contained — scroll never leaks past it. */
    overscroll-behavior-y: contain;
    overscroll-behavior-x: none;
  }

  /* ── Cell N: presentation-surface viewport-constrained wrapper ──
     MOBILE_SCAFFOLD_CONTRACT §2 matrix row N. The scaffold adds this class to
     a modal/view contentEl when its mount config carries
     `presentationSurface: true` (mobile-shell.js _applyPresentationFrame).
     Purpose: box a presentation surface (Pocket Stage player, About-story
     Atelier, review harness) to the Cell-E anchored envelope — top-nav to
     tab-bar — so the stage fills exactly the available viewport and never
     escapes the chrome bands. Unlike .ffa-mobile-modal-chrome (a scrollable
     modal body), a presentation is viewport-LOCKED: overflow hidden, no
     internal page scroll. Additive + inert on desktop (this whole block is
     @media (max-width: 767px)); the class is removed on unmount so a reused
     contentEl never leaks the frame. */
  .ms-presentation-frame {
    position: fixed !important;
    top: var(--mobile-top-bar-total-height) !important;
    bottom: var(--mobile-tab-bar-total-height) !important;
    left: 0 !important;
    right: 0 !important;
    width: 100% !important;
    max-width: 100% !important;
    padding-left: var(--mobile-safe-area-left);
    padding-right: var(--mobile-safe-area-right);
    box-sizing: border-box;
    /* Viewport-locked: the stage owns its own internal layout; the page
       chain never scrolls behind it. */
    overflow: hidden;
    overscroll-behavior: none;
  }

  /* ── Safe area: ensure bottom-fixed elements clear home indicator ── */

  .modal-footer {
    padding-bottom: calc(12px + var(--mobile-safe-area-bottom));
  }

  /* fluffy-puzzling-dongarra S1 D2: bidirectional viewport anchoring at the
     browser-engine layer. `overscroll-behavior-y: contain` stops vertical
     elastic overscroll from leaking past the modal into the body chain;
     `overscroll-behavior-x: none` kills horizontal rubber-band entirely
     (the long-standing right-pan drift in Joe's 2026-05-10 walkthrough
     Issue #3). Applied to <body> for the page-level chain AND to every
     modal-class scroll container so a modal-scoped scroll never propagates.
     This is the engine-level companion to the CSS-bounds fix in D1+D3. */
  body {
    overscroll-behavior-y: contain;
    overscroll-behavior-x: none;
  }

  .unified-ai-modal,
  .unified-ai-container,
  .browse-media-modal,
  .curation-modal-content,
  .unified-ai-messages,
  .media-grid-container,
  .bm-grid-container,
  .ml-grid {
    overscroll-behavior-y: contain;
    overscroll-behavior-x: none;
  }

  /* ==========================================================================
     Phase 2: Mobile Top Bar
     ========================================================================== */

  /* ── Top nav transform ── */
  .top-nav {
    height: var(--mobile-top-bar-total-height);
    padding-top: var(--mobile-safe-area-top);
    padding-left: max(12px, var(--mobile-safe-area-left));
    padding-right: max(12px, var(--mobile-safe-area-right));
    /* cementing-modal-chrome-fleet S1 D1 (2026-05-13) — Iron-Law smoking-gun
       fix at the mobile-scoped layer. Pre-S1 the desktop base rule
       (styles.css:143 `background: white`) WON on mobile whenever
       body[data-modal-palette] was unset OR carried a value with no matching
       palette override — Settings, Admin, About, Leaderboard, dashboard
       cold-load, AND any navigation through tab-bar.js onNavigate()'s
       `else { removeAttribute }` branch all hit this trap. The base white
       paint was the smoking gun, not the per-palette absences. S1 sets the
       mobile-scoped .top-nav default to var(--ffa-band-default, #0a2e2e)
       (matches the static <meta name="theme-color" content="#0a2e2e"> at
       index.html:28). Every existing `body[data-modal-palette="..."]
       .top-nav { background: ... }` rule below continues to win at
       specificity (0,2,1) > this rule (0,1,0). When palette is unset OR
       unknown, the mobile band now paints dark teal — visually belongs with
       FFA chrome instead of jarring white. Color flips to light cream
       (#f5f5f7) so the title text + hamburger glyph stay legible against
       the dark default. Paired with D3's onNavigate() defaulting branch
       (every mobile view sets data-modal-palette on mount) the
       white-band state is structurally unreachable on mobile. */
    background: var(--ffa-band-default, #0a2e2e);
    color: #f5f5f7;
    /* realizing-mobile-modals S3 D5 (Issue #10, 2026-05-13) — FFA Window
       Frame Header-always-visible-and-tappable invariant: the global .top-nav
       must float above every modal overlay layer on mobile so that hamburger
       + Fergi AI + title slot remain visible and reachable from inside any
       modal (UMI z:10400, .se-overlay constellation z:10500, .mobile-tab-bar
       z:11000). Z:11500 places .top-nav above the modal+tab-bar layer but
       BELOW .mobile-drawer (z:12000) — the hamburger drawer is the universal
       nav hub and intentionally covers .top-nav with its own internal top-bar
       (per mobile-drawer.js _wireHamburgerButton + .mobile-drawer-topbar).
       Pre-fix desktop z:100 left .top-nav buried under .um-overlay / .se-
       overlay on iPhone — hamburger + Fergi AI tap was silently swallowed by
       the modal layer, which is the surfaced symptom in Joe's 2026-05-13
       /listen Issue #10 (controls dead after constellation drill). With this
       fix the controls are tappable at every navigation depth — root cause
       resolution per Iron Law (not a defensive re-bind). The msp-float-
       controls inside the UMI focal panel (close / maximize / download) sit
       at top:env(safe-area-inset-top)+8px, which on iPhone lands BELOW the
       .top-nav band — so the floating controls do not collide with the
       hamburger/Fergi AI band; they sit beneath it on the focal media
       letterbox. Presentation-mode UMI (Story playback) hides .top-nav via
       FrameController.maximize(body.pane-maximized translateY(-100%) +
       pointer-events:none) — unaffected by this z-index lift. */
    z-index: 11500;
  }

  /* Main content top margin must match the top-nav band height EXACTLY,
     including safe-area-inset-top, so the hamburger drawer's parent header
     band is fully visible on iPhone (Dynamic Island / notch clearance). The
     desktop rule (`margin-top: var(--nav-height)` = 60px, NO safe-area) is
     correct for desktop but wrong for iPhone. binary-hatching-pearl D1. */
  .main-content {
    margin-top: var(--mobile-top-bar-total-height) !important;
  }

  /* ── Left: hamburger replaces home emoji + nav title on mobile.
        S1 harnessing-iphone-flotilla — the persistent home affordance is now
        the hamburger drawer (.mobile-hamburger-btn). The legacy nav-logo emoji
        and the desktop nav title are hidden on mobile. ── */
  .nav-left .nav-title {
    display: none;
  }

  .nav-left .nav-logo {
    display: none !important;
  }

  /* ── Center: Hide focus chips, show mobile view title ── */
  .nav-center .focus-chips-container {
    display: none;
  }

  .mobile-view-title {
    display: block;
    font-size: calc(17px * var(--type-scale, 1));
    font-weight: 600;
    text-align: center;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
    /* embracing-iphone-immersion S1 D4 (2026-05-12) — Dynamic Island
       horizontal carve-out. Pre-S1 a hard `max-width: 220px` cap was the
       only width constraint; it kept the title narrow on every iPhone
       regardless of viewport width and gave no structural reservation for
       the Dynamic Island's horizontal projection in the iOS status bar
       gutter above. The new dynamic carve-out reserves ~160px center (80px
       each side) for visual clearance from the Island while letting the
       title slot scale up on wider iPhones — iPhone 15 Pro Max gets ~270px
       of title slot, iPhone SE gets ~215px. On non-Island iPhones the
       carve-out is a harmless ~160px of unused band; the visual effect is
       a slightly more centered title with breathing room on both sides.
       The min() floor with 220px preserves the historical cap as the upper
       bound where the dynamic value would exceed it on very wide
       viewports — keeping the title visually proportionate. */
    /* ⛔ `100vw`, NOT `100%` — and the difference is the whole carve-out
       (surfacing-desktop-rooms-mobile S2, 2026-09-21).

       MEASURED, webkit-iphone 390x844: this slot renders **80 px wide on every
       iPhone**, not the "~215-270px" the paragraph above describes. The
       percentage resolved against `.nav-center`, which is
       `position: absolute; min-width: max-content` (styles.css:404-418) — a
       SHRINK-TO-FIT box whose own width comes from this child. So
       `calc(100% - 160px)` resolved against a width that was itself decided by
       this rule's `min-width: 80px`, and the carve-out collapsed onto its floor.
       Positive control: "Fergi AI" measures exactly 80 px — at the floor, not
       near it — while "Dressing Room" measures scrollWidth 124 against
       clientWidth 80 and renders "Dressin…".

       Nobody met the wall because every title this band has carried is <= 8
       characters. `VIEW_TITLES`' own "Import Media" (mobile-shell.js) already
       clips today, and the phone's Dressing Room cannot name itself at all —
       which the census reads as a C1 structure failure ("no title in the
       scaffold title slot"), so the choice was a clipped name or a red row.

       `100vw` resolves against the viewport, which is what the Dynamic Island
       carve-out was always describing. Nothing under 80 px changes: the floor
       still holds the slot open on cold mount, the box stays centred by
       `.nav-center`'s own left:50%/translateX(-50%), and only titles that were
       being truncated get their declared width. Measured after: 230 px at 390,
       every existing view title unchanged. */
    max-width: min(280px, calc(100vw - 160px));
    /* crisp-anchoring-cradle S1 D1-A: a min-width guarantees the .nav-center
       parent (position: absolute, min-width: max-content per styles.css :240)
       reserves visible width even when the text is short — so the centered
       title never collapses to zero on cold mount. */
    min-width: 80px;
    margin-inline: auto;
    /* Inherit currentColor from .top-nav so the band's data-modal-palette
       tinting (below) cascades to the title text without a per-palette rule. */
    color: inherit;
  }

  /* mending-iphone-debut S2 D1 (2026-05-15) — Cell P bottom-flush enforcement.
     Foundation CSS rule: any element that scaffold marks `.ms-bottom-flush`
     anchors to bottom: 0 with the home-indicator area painted via
     padding-bottom: env(safe-area-inset-bottom). `.mobile-tab-bar` already
     satisfies this contract structurally (mobile-tab-bar.css:181-204) — this
     rule extends the same invariant to any future bottom-anchored modal
     surface that opts in via the scaffold's `.ms-bottom-flush` class apply.
     Paired JS in mobile-shell.js force-reflows these elements on every
     visualViewport.resize so iOS Safari re-evaluates env() when the URL bar
     collapses/expands or the orientation changes. Root cause + safeguard
     documented in MOBILE_SCAFFOLD_CONTRACT.md § Cell P. */
  .ms-bottom-flush {
    position: fixed !important;
    bottom: 0 !important;
    left: 0;
    right: 0;
    /* 2026-05-16 Iron-Law: env() direct, not through --mobile-safe-area-bottom.
       The previous var-with-env-fallback chain looked safe but the var IS
       defined (mobile.css:80) so the fallback never fires — and iOS Safari
       captures env() in the custom property at :root parse time, sticking it
       at 0px on cold mount. Same root cause as the tab-bar fix at
       mobile-tab-bar.css:199. */
    padding-bottom: env(safe-area-inset-bottom, 0px) !important;
  }

  /* mending-iphone-debut S2 D2 (2026-05-15) — Cell Q top-band focus-identifier
     slot. The plan-file referred to this as "Cell J", but Cell J in the
     MOBILE_SCAFFOLD_CONTRACT is already taken (Fergi AI launcher); renamed
     to Cell Q to preserve the existing constitutional cell-naming. Iron-Law
     Three-Question audit: (a) cause = plan authoring missed the existing
     contract's cell-letter assignments; (b) the rename to Cell P/Q closes
     the same drift class for any future cell additions (always check the
     contract's matrix before assigning a new letter); (c) safeguard = the
     contract's Section 2 matrix is now the canonical letter registry.

     The focus-identifier is a smaller secondary line beneath the title in
     the .nav-center. It surfaces view-scoped state: Library's active filter
     ("Technical · 44 docs"), Family's focal name, future UMI media metadata.
     Empty by default; CSS :empty selector below collapses the slot to zero
     height so views that don't opt in retain the historic single-line layout.

     .nav-center must flex-column to stack title + focus-identifier vertically.
     The desktop base rule sets row direction (styles.css:242). The mobile
     override below flips direction without touching desktop. */
  .nav-center {
    flex-direction: column !important;
    /* gap intentionally zero — line-heights of the two children provide
       visual breathing room; an explicit gap would push the secondary line
       below the band's vertical centerline. */
    gap: 0;
  }

  .mobile-view-focus-identifier {
    display: block;
    font-size: calc(12px * var(--type-scale, 1));
    font-weight: 400;
    line-height: 14px;
    text-align: center;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
    /* Mirror the title's dynamic-island carve-out so long focus-identifier
       text doesn't crowd the iOS Dynamic Island gutter on iPhone 15 Pro Max.
       ⚠️ `100vw` for the same reason as the title above — this line had the same
       percentage against the same shrink-to-fit parent, so it collapsed to the
       same 80 px floor, and it is the line that carries "Technical · 44 docs". */
    max-width: min(280px, calc(100vw - 160px));
    min-width: 80px;
    margin-inline: auto;
    margin-top: 1px;
    /* Secondary text reads softer than the primary title — opacity carries
       the visual hierarchy without a per-palette color override. Inherits
       currentColor from .top-nav so palette tinting cascades. */
    color: inherit;
    opacity: 0.72;
  }

  /* Empty slot collapses to zero so views that don't opt in retain the
     historic single-line layout. The :empty selector matches when the
     element has no children AND no text content — exactly what
     setFocusIdentifier('') produces. */
  .mobile-view-focus-identifier:empty {
    display: none;
  }

  /* crisp-anchoring-cradle S1 D1-A — Universal Modal Chrome Contract: TOP BAND
     palette match. Mirrors the BOTTOM BAND palette pattern shipped in
     reflective-swimming-ripple S1 D1-D (which uses body[data-modal-palette=...]
     on the .mobile-tab-bar). Without this, every modal's dark-teal/indigo body
     read as a "panel below a white band" on iPhone — the contract memory's
     "modal panel meets band, no gap" requirement was failing at the color
     boundary. tab-bar.js already sets body[data-modal-palette] on every
     onNavigate() call; this rule is consumer-only.

     Palette mapping (closing-realization-gate S2 D5 distinct-palette lock,
     2026-05-13 PM — Joe-approved via S1 D8 AskUserQuestion):
       stories     → plum    #2d1b3d  (complements narrative-text + cover-art)
       media       → forest  #1a3d2e  (complements photographic-grid content)
       family      → dark teal #0a2e2e (matches .fnav-substrate)
       library     → espresso #2d1f15 (complements document/book-card content)
     Per universal-pattern rule #7 (per-modal DISTINCT palette) + rule #8
     (chrome COMPLEMENTS main content). Pre-S2 Stories/Media/Library shared
     indigo #1a1a2e — fails distinctness; replaced by per-modal palettes.
     Top-band height includes safe-area-inset-top, so the palette extends
     into the iPhone notch / Dynamic Island gutter — eliminating the
     "white horizontal gap" Joe saw between band and modal body.

     Title + hamburger + Fergi AI launcher inherit `color: inherit` so they
     re-tint per palette without per-palette overrides. */
  body[data-modal-palette="stories"] .top-nav {
    background: #2d1b3d;
    border-bottom-color: rgba(255, 255, 255, 0.08);
    color: #f5f5f7;
  }
  body[data-modal-palette="media"] .top-nav {
    background: #1a3d2e;
    border-bottom-color: rgba(255, 255, 255, 0.08);
    color: #f5f5f7;
  }
  body[data-modal-palette="library"] .top-nav {
    background: #2d1f15;
    border-bottom-color: rgba(255, 255, 255, 0.08);
    color: #f5f5f7;
  }
  body[data-modal-palette="family"] .top-nav {
    background: #0a2e2e; /* matches --fnav-substrate */
    border-bottom-color: rgba(255, 255, 255, 0.08);
    color: #f5f5f7;
  }
  /* sweeping-unifying-canopy S1 D2/D6 (Issue #2 + #19a, 2026-05-11) — Fergi AI
     pink top-band palette. Joe directive 2026-05-11 /listen Issue #19: "Fergi
     AI modal: header + bottom panel pink." Pink-900 (#831843) matches the
     deep-saturation envelope of the existing indigo (#1a1a2e) and dark-teal
     (#0a2e2e) palettes — readable as white text, brand-coherent with the
     Fergi AI purple ripple highlight (purple → pink is a natural neighbor on
     the warm-cool spectrum). Sets the modal's palette identity at the top
     band; the matching bottom-band tint will land in S2 D6. */
  body[data-modal-palette="fergi-ai"] .top-nav {
    background: #831843;
    border-bottom-color: rgba(255, 255, 255, 0.08);
    color: #f5f5f7;
  }
  /* embracing-iphone-immersion S1 D6 (2026-05-12) — Train Station palette.
     Joe directive: Train Station migrates onto the universal chrome contract
     with its distinctive blue palette preserved. #0f3057 matches the base
     anchor of the .ts-modal-header linear-gradient
     (linear-gradient(135deg, #0f3057 0%, #1f5f8b 50%, #2c7a9c 100%)) — so
     the FFA top band paints continuous with the in-modal TS header below it.
     The data-modal-palette attribute is set in train-station-modal.js on
     openTrainStation() and cleared on closeTrainStation(). */
  body[data-modal-palette="train-station"] .top-nav {
    background: #0f3057;
    border-bottom-color: rgba(255, 255, 255, 0.12);
    color: #f5f5f7;
  }
  /* cementing-modal-chrome-fleet S1 D2 (2026-05-13) — Settings palette
     completes the universal contract for mobile-settings-view. Deep slate
     #1e293b is structurally distinct from teal (#0a2e2e Family) / indigo
     (#1a1a2e Stories/Media/Library) / wine (#831843 Fergi AI) / TS-blue
     (#0f3057), so Settings reads as its own surface in the modal-palette
     vocabulary. MobileSettingsView.init() sets data-modal-palette='settings'
     on mount; destroy() restores. The bottom-band tint lives in
     mobile-tab-bar.css's matching `body[data-modal-palette="settings"]`
     rule. Title text + hamburger glyph + Fergi AI launcher inherit color
     light cream so they read against the slate band. */
  body[data-modal-palette="settings"] .top-nav {
    background: #1e293b;
    border-bottom-color: rgba(255, 255, 255, 0.08);
    color: #f5f5f7;
  }
  /* cementing-modal-chrome-fleet S1 D3 (2026-05-13) — 'default' palette
     rule. Pre-S1 the THEME_COLORS map contained 'default' but the CSS
     selector did not — so the iOS status bar tinted teal correctly for
     palette-less views, but the in-page .top-nav band stayed white (via
     the base rule). D3 changes tab-bar.js onNavigate() so views without a
     VIEW_TO_TAB entry set data-modal-palette='default' instead of stripping
     the attribute. This CSS rule paints those views' .top-nav band dark
     teal, completing the structural elimination of the white-band state.
     Applies to: dashboard, admin, about, leaderboard, sandbox, and any
     future view that doesn't claim its own palette. */
  body[data-modal-palette="default"] .top-nav {
    background: var(--ffa-band-default, #0a2e2e);
    border-bottom-color: rgba(255, 255, 255, 0.08);
    color: #f5f5f7;
  }
  body[data-modal-palette="train-station"] {
    background: #0f3057;
  }
  /* D5: hamburger + Fergi AI launcher inherit color: inherit (light cream)
     so they read against the TS-blue band. Already covered by the existing
     palette-aware rules below; this comment is the marker for the D5
     verification grep target. */
  body[data-modal-palette="train-station"] .mobile-hamburger-btn,
  body[data-modal-palette="train-station"] .mobile-fergi-ai-launcher {
    color: #f5f5f7;
  }
  body[data-modal-palette="train-station"] .mobile-fergi-ai-launcher:active {
    background: rgba(255, 255, 255, 0.10);
  }
  /* Hamburger drawer palette — when the global drawer is open, the top band
     reads as part of the drawer's dark-teal Fergi AI brand surface (the
     drawer-internal top-bar matches via .mobile-drawer-topbar). Body class
     `mobile-drawer-open` already toggles when the drawer mounts; we drive
     the palette off the same signal so no consumer wiring is required. */
  body.mobile-drawer-open .top-nav {
    background: #0a2e2e;
    border-bottom-color: rgba(255, 255, 255, 0.08);
    color: #f5f5f7;
  }
  /* Mobile hamburger + Fergi AI launcher buttons inherit text color from
     .top-nav (currentColor stroke / fill) — so they automatically re-tint.
     Override the :active ripple highlight to a palette-appropriate tone. */
  body[data-modal-palette="stories"] .mobile-hamburger-btn,
  body[data-modal-palette="media"] .mobile-hamburger-btn,
  body[data-modal-palette="library"] .mobile-hamburger-btn,
  body[data-modal-palette="family"] .mobile-hamburger-btn,
  body[data-modal-palette="fergi-ai"] .mobile-hamburger-btn,
  body[data-modal-palette="stories"] .mobile-fergi-ai-launcher,
  body[data-modal-palette="media"] .mobile-fergi-ai-launcher,
  body[data-modal-palette="library"] .mobile-fergi-ai-launcher,
  body[data-modal-palette="family"] .mobile-fergi-ai-launcher {
    color: #f5f5f7;
  }
  body[data-modal-palette="stories"] .mobile-fergi-ai-launcher:active,
  body[data-modal-palette="media"] .mobile-fergi-ai-launcher:active,
  body[data-modal-palette="library"] .mobile-fergi-ai-launcher:active,
  body[data-modal-palette="family"] .mobile-fergi-ai-launcher:active {
    background: rgba(255, 255, 255, 0.10);
  }
  /* sweeping-unifying-canopy S1 D6 (Issue #20, 2026-05-11) — Closed Privacy
     Invariant of recursion: when the Fergi AI modal is open, the .nav-right
     Fergi AI launcher is suppressed. Joe directive: "no recursive Fergi AI
     control in Fergi AI modal." Tapping the launcher from inside the modal
     would be a no-op that confuses the affordance contract — the Fergi AI
     surface is already foregrounded, so the launcher's role is occupied. The
     surrounding chrome (hamburger, title) remains for navigation continuity. */
  body[data-modal-palette="fergi-ai"] .mobile-fergi-ai-launcher {
    display: none !important;
  }
  /* Match the body background to the modal palette so any residual gap
     between top-band, .main-content, and modal panel paints as the modal
     color instead of white. This is the belt to the top-nav rules'
     suspenders — guarantees the "modal panel meets band, no white gap"
     contract requirement even if a per-modal CSS file regresses padding. */
  body[data-modal-palette="family"] {
    background: #0a2e2e;
  }
  /* Portraits (Greg G-146): the room's own ground under band, body and tab bar — no grey strip. */
  body[data-modal-palette="portraits"] {
    background: #071c1e;
  }
  /* The phone band already says "Portraits": the room's own 🖼️ heading repeated it flush against the glass
     (x 0, the emoji stacked over the word). Keep the one-line subtitle as the room's introduction. */
  #view-portraits > .view-header { padding: 6px 16px 2px; margin: 0; gap: 0; }
  #view-portraits > .view-header h2 { display: none; }
  #view-portraits > .view-header p { margin: 0; font-size: calc(0.92rem * var(--type-scale, 1)); color: var(--ffvr-ink-muted, #b6c4c4); }
  body[data-modal-palette="portraits"] .top-nav {
    background: #071c1e;
    border-bottom-color: rgba(63, 184, 178, 0.28);
    color: #f0fcfb;
  }
  body[data-modal-palette="fergi-ai"] {
    background: #831843;
  }
  /* closing-realization-gate S2 D2 (2026-05-13 PM) — H3 fix: body bg
     unified with bar bg per modal so any rendering gap between
     .main-content bottom and .mobile-tab-bar top paints same color
     instead of cream. Per-palette distinct values land here per S2 D5
     plum/forest/espresso lock. */
  body[data-modal-palette="stories"] {
    background: #2d1b3d;
  }
  body[data-modal-palette="media"] {
    background: #1a3d2e;
  }
  /* sweeping-unifying-canopy S1 D4 (Issue #14, 2026-05-11) — Library vertical
     anchor: kill the cream gap between the dark-indigo top band and the
     dark-teal `.mdl-wrap` content. The Library mobile view (mdl-wrap)
     intentionally uses #0a2e2e as its surface (Library = brand-coherent with
     the Family dark-teal substrate per mobile-doc-library.css:32). The prior
     body rule painted body=#f8f9fa for library, which surfaced as a "cream"
     hairline anywhere the band-or-wrap left a single-pixel gap (e.g., during
     mount animation, scroll-bounce, safe-area inset rounding). Painting the
     body the same dark-teal as the wrap means the gap — if any — paints as
     the modal color, not cream. Iron-Law fix: source of cream-gap was the
     body-vs-wrap color mismatch, not a specific padding bug.

     closing-realization-gate S2 D5 (2026-05-13 PM) — Library upgrades from
     shared teal to distinct espresso #2d1f15 per Joe-approved S1 D8 lock
     (rule #7 per-modal distinct palette). The .mdl-wrap rule in
     mobile-doc-library.css also updates to espresso to match. */
  body[data-modal-palette="library"] {
    background: #2d1f15;
  }

  /* sweeping-unifying-canopy S1 D4 (Issue #16, 2026-05-11) — Download Bundle
     header-slot affordance for the Library tab. Joe directive 2026-05-11
     /listen: "Library Download Bundle in header slot." Renders as a compact
     right-aligned link inside `.mdl-header`, paired with the legacy
     `.mdl-bundle-banner` (now hidden) — the link populates via
     `_renderBundleBanner()` whenever the bundle manifest yields a download
     for the active lib (User / Technical / Biography). Closed Privacy
     Invariant compliance: the label is "Bundle" / "Book" (egress = Download),
     never "Share." */
  .mdl-bundle-header-btn {
    flex-shrink: 0;
    display: inline-flex;
    align-items: center;
    padding: 6px 10px;
    border-radius: 16px;
    background: rgba(255, 255, 255, 0.10);
    border: 1px solid #1a5252;
    color: #e0e0e8;
    font-size: calc(12px * var(--type-scale, 1));
    font-weight: 600;
    text-decoration: none;
    white-space: nowrap;
    -webkit-tap-highlight-color: transparent;
  }
  .mdl-bundle-header-btn:active {
    background: rgba(255, 255, 255, 0.18);
  }
  .mdl-bundle-header-btn[hidden] {
    display: none !important;
  }

  /* sweeping-unifying-canopy S1 D5 (Issue #12, 2026-05-11) — Family headshot
     pulled-up under the chrome top band. Marker class `.fnav-pulled-up` is
     set on `.fnav-root` by family-mobile.js's init() when the chrome-contract
     layout is active (always, on mobile). Removes the duplicate-title row
     vertical real estate so the focal portrait sits closer to the band, per
     Joe's 2026-05-11 /listen Issue #12: "Family modal name in centered slot +
     headshot pulled up." The modal-internal nav row keeps just the search
     button (right) + breadcrumb; no title row competing with the band. */
  .fnav-pulled-up .fnav-nav-row {
    justify-content: flex-end;
    min-height: 0;
    padding-top: 4px;
    padding-bottom: 0;
  }
  .fnav-pulled-up .fnav-nav {
    padding-top: 0;
  }
  /* realizing-mobile-chrome-contract S2 D2 (2026-05-14) — biography block
     flush against chrome header band. Joe iPhone /listen Issue #3 sub-item:
     focal photo + bio buttons must start IMMEDIATELY below the chrome band
     (no 4px discretionary gap). Pre-S2 `padding-top: 4px` left a hairline
     between the .fnav-nav-row (carrying the new Family Navigator launcher)
     and the focal hero card. Setting to 0 closes the gap — the .fnav-nav-row
     itself retains its 4px top breathing room (mobile.css line 514) for the
     launcher button's tap target, but the scroll surface below it is flush. */
  .fnav-pulled-up .fnav-scroll {
    padding-top: 0;
  }

  /* ── Right: Fergi AI launcher replaces the legacy JF avatar circle.
        fluffy-puzzling-dongarra S2 D1 (2026-05-10) — Issue #1 unified top-bar
        chrome contract: every mobile surface now reads hamburger-left + Fergi
        AI icon-right. The old `.user-menu-rect` (rectangular "Joe ▼" + circular
        initials avatar) is suppressed on mobile, along with its dropdown.
        User-settings affordances (My Profile, Settings, Sign out, About) live
        in the hamburger drawer's ACCOUNT section. The AI launcher button is
        injected into `.nav-right` by mobile-drawer.js on shell init.
        Iron-Law class fix: `!important` is necessary because stories.css
        (loaded after mobile.css) re-asserts `.user-menu-rect` visibility on
        the mobile breakpoint as part of the binary-hatching-pearl D1 cascade
        repair. Without `!important` the JF avatar would still render. ── */
  .nav-right .user-menu-rect,
  .nav-right .user-name-btn,
  .nav-right .user-initials,
  .nav-right .user-dropdown {
    display: none !important;
  }

  /* Fergi AI launcher button — sits in `.nav-right` on every mobile surface.
     Visual peer to `.mobile-hamburger-btn` (44×44 tap target, currentColor
     stroke, transparent background, identical active ripple). The sparkle
     glyph is the canonical AI affordance per ffa-icons.js but inlined here
     so the button is self-contained (matches the hamburger SVG-inline
     pattern at index.html:113-119). */
  .mobile-fergi-ai-launcher {
    width: 44px;
    height: 44px;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    background: transparent;
    border: none;
    color: #1f2937;
    cursor: pointer;
    border-radius: 22px;
    -webkit-tap-highlight-color: transparent;
    padding: 0;
    margin: 0;
  }

  .mobile-fergi-ai-launcher:active {
    background: rgba(124, 58, 237, 0.14);
    color: #7c3aed;
  }

  .mobile-fergi-ai-launcher svg {
    width: 26px;
    height: 26px;
    display: block;
    /* crisp-anchoring-cradle S1 D1-B: round the canonical favicon's square
       PCB background so the brand mark reads as a tile rather than a hard-
       edged box inside the circular tap target. Faint outline gives a clean
       delineation against the data-modal-palette tinted .top-nav backgrounds
       (which can be near-identical to the favicon's own #0a2e2e fill on the
       family palette). */
    border-radius: 6px;
    box-shadow: 0 0 0 1px rgba(255, 255, 255, 0.16);
  }


  /* ==========================================================================
     Phase 2: Hide sidebar on mobile (replaced by TabBar + Home Sheet)
     ========================================================================== */

  .side-nav {
    display: none !important;
  }

  /* Remove mobile control panel backdrop since sidebar is gone */
  .mobile-control-panel-backdrop {
    display: none !important;
  }

  /* Main content takes full viewport width minus safe-area insets.
     S1 harnessing-iphone-flotilla — realises mobile-parity-launch anchor #2
     (full screen width). Desktop sidebar offset removed.

     binary-hatching-pearl D2 Iron-Law class fix (2026-05-09): horizontal
     padding floor of 12px removed. Joe's 2026-05-09 iPhone Safari review
     surfaced "many screens float in inset margins instead of full-bleed" —
     the 12px floor on .main-content was visible on every view, compounded
     by view-wrapper L/R padding (mdl-wrap, etc.). New rule: L/R padding is
     env(safe-area-inset-*) only at mobile, no extra floor. Vertical padding
     preserved (top via mobile-top-bar margin; bottom per surface). */
  .main-content {
    margin-left: 0 !important;
    padding-left: var(--mobile-safe-area-left) !important;
    padding-right: var(--mobile-safe-area-right) !important;
    width: 100%;
    max-width: 100% !important;
    box-sizing: border-box;
  }

  /* realizing-mobile-modals S1 D1 (2026-05-13) — anchor modal panel.
     Issues #1 + #5 of handoff: user could finger-drag the entire Family /
     Stories modal panel downward, exposing a gap between the iPhone header
     chrome (`.top-nav`) and the modal's content. overscroll-behavior:
     contain on body + the inner scroll surfaces (`.fnav-scroll`,
     `.main-content`'s subviews) was in place from
     reflective-swimming-ripple S1 D1-C, but iOS Safari can still translate
     the whole flow during its address-bar reveal / pull-down gesture
     because .main-content was still flow-positioned with margin-top under
     a fixed .top-nav. The structural anchor: pin .main-content between
     the top-band and tab-bar bands via position:fixed and make
     .main-content itself the scroll surface. Removes the body-level drag
     path. .top-nav (fixed top:0) + .mobile-tab-bar (fixed bottom:0)
     bookend the inter-band region; .main-content fills exactly that
     region. Replaces the prior `margin-top: var(--mobile-top-bar-total-height)`
     rule (line ~163-165) — the !important position-anchor wins on the
     cascade. Honored by every tab view (#view-stories, #view-media,
     #view-family-navigator, #view-documentation) and by every flow-
     positioned child surface; modal-class fixed overlays
     (.unified-ai-modal, .browse-media-modal, etc.) layer on top per their
     own position:fixed rules and are not constrained by this. */
  .main-content {
    position: fixed !important;
    top: var(--mobile-top-bar-total-height) !important;
    bottom: var(--mobile-tab-bar-total-height) !important;
    left: 0 !important;
    right: 0 !important;
    margin-top: 0 !important;
    overflow-y: auto;
    -webkit-overflow-scrolling: touch;
    overscroll-behavior-y: contain;
    overscroll-behavior-x: none;
  }

  /* When the tab bar is hidden (presentation playback, etc.) the bottom
     anchor relaxes so the active surface fills to the safe-area-bottom.
     `body.has-mobile-tab-bar` is added/removed by tab-bar.js create()/
     destroy(); the `:not()` carve-out gives presentation-mode the
     full-bleed bottom it needs without touching the per-surface .hide()
     plumbing. */
  body:not(.has-mobile-tab-bar) .main-content {
    bottom: var(--mobile-safe-area-bottom, 0px) !important;
  }

  /* ==========================================================================
     Phase 2: Home BottomSheet Styles
     ========================================================================== */

  .mobile-home-sheet {
    padding: 4px 0;
  }

  .home-sheet-section {
    padding: 4px 0;
  }

  .home-sheet-section + .home-sheet-section {
    border-top: 1px solid #f3f4f6;
    margin-top: 4px;
    padding-top: 8px;
  }

  .home-sheet-section-title {
    font-size: calc(11px * var(--type-scale, 1));
    font-weight: 600;
    color: #9ca3af;
    text-transform: uppercase;
    letter-spacing: 0.05em;
    padding: 4px 20px 6px;
  }

  .home-sheet-item {
    display: flex;
    align-items: center;
    gap: 14px;
    width: 100%;
    padding: 13px 20px;
    border: none;
    background: none;
    cursor: pointer;
    -webkit-tap-highlight-color: transparent;
    text-align: left;
    font-size: calc(16px * var(--type-scale, 1));
    color: #1f2937;
    transition: background 0.1s;
  }

  .home-sheet-item:active {
    background: #f3f4f6;
  }

  .home-sheet-item-icon {
    font-size: calc(20px * var(--type-scale, 1));
    width: 28px;
    text-align: center;
    flex-shrink: 0;
  }

  .home-sheet-item-label {
    font-weight: 450;
  }

  /* ── Profile row in Home Sheet ── */
  .home-sheet-profile-row {
    display: flex;
    align-items: center;
    gap: 12px;
    padding: 14px 20px;
    cursor: pointer;
    -webkit-tap-highlight-color: transparent;
    background: #f0fdf4;
    border-radius: 12px;
    margin: 4px 12px;
  }

  .home-sheet-profile-row:active {
    background: #dcfce7;
  }

  .home-sheet-profile-icon {
    font-size: calc(18px * var(--type-scale, 1));
  }

  .home-sheet-profile-text {
    flex: 1;
    font-size: calc(15px * var(--type-scale, 1));
    font-weight: 500;
    color: #065f46;
  }

  .home-sheet-profile-arrow {
    font-size: calc(20px * var(--type-scale, 1));
    color: #6b7280;
  }

  /* ==========================================================================
     Phase 2: Profile Sheet Styles
     ========================================================================== */

  .mobile-profile-sheet {
    padding: 8px 0;
  }

  .profile-sheet-row {
    display: flex;
    flex-direction: column;
    gap: 4px;
    padding: 16px 20px;
    cursor: pointer;
    -webkit-tap-highlight-color: transparent;
    border-bottom: 1px solid #f3f4f6;
    transition: background 0.1s;
  }

  .profile-sheet-row:last-child {
    border-bottom: none;
  }

  .profile-sheet-row:active {
    background: #f9fafb;
  }

  .profile-sheet-row-header {
    display: flex;
    align-items: center;
    gap: 10px;
  }

  .profile-sheet-row-icon {
    font-size: calc(20px * var(--type-scale, 1));
  }

  .profile-sheet-row-title {
    font-size: calc(16px * var(--type-scale, 1));
    font-weight: 500;
    color: #1f2937;
  }

  .profile-sheet-row-value {
    padding-left: 30px;
    font-size: calc(14px * var(--type-scale, 1));
  }

  .profile-sheet-none {
    color: #9ca3af;
  }

  .profile-sheet-active {
    color: #059669;
    font-weight: 500;
  }

  /* ==========================================================================
     Phase 5-6: Browse Media + Family Navigator verify (minimal fixes)
     ========================================================================== */

  /* Browse Media modal: fills area between header and tab bar */
  .browse-media-modal.active {
    top: calc(var(--mobile-top-bar-height) + var(--mobile-safe-area-top)) !important;
    bottom: calc(var(--mobile-tab-bar-height) + var(--mobile-safe-area-bottom)) !important;
    z-index: 10000;
  }

  /* Family Navigator person panel: ensure full-width on mobile */
  .person-panel-enhanced.active {
    width: 100% !important;
    right: 0 !important;
    z-index: 500;
  }

  /* ==========================================================================
     Desktop-only surfaces: graceful banner on mobile
     ========================================================================== */

  .desktop-only-banner {
    display: block;
    text-align: center;
    padding: 10px 16px;
    background: #fef3c7;
    color: #92400e;
    font-size: calc(13px * var(--type-scale, 1));
    font-weight: 500;
    border-radius: 8px;
    margin: 8px 12px;
  }

  /* ==========================================================================
     Phase 4: Fergi AI Mobile Fixes
     ========================================================================== */

  /* ── AI modal: fills area between header and tab bar ── */
  .unified-ai-modal:is(.active, .is-open) {
    top: calc(var(--mobile-top-bar-height) + var(--mobile-safe-area-top)) !important;
    left: 0 !important;
    right: 0 !important;
    bottom: calc(var(--mobile-tab-bar-height) + var(--mobile-safe-area-bottom)) !important;
    width: 100% !important;
    height: auto !important;
  }

  .unified-ai-container {
    height: 100% !important;
    max-height: 100% !important;
    display: flex !important;
    flex-direction: column !important;
  }

  /* ── Messages area: flexible height, smooth scrolling ── */
  .unified-ai-messages {
    flex: 1 !important;
    overflow-y: auto !important;
    -webkit-overflow-scrolling: touch !important;
    overscroll-behavior-y: contain;
    min-height: 0 !important;
  }

  /* ── Input area: sticky at bottom with safe area ── */
  .unified-ai-input-area {
    position: sticky !important;
    bottom: 0;
    padding-bottom: calc(12px + var(--mobile-safe-area-bottom)) !important;
    background: #fff !important;
    z-index: 10;
    flex-shrink: 0;
  }

  /* ── present_media results: nested inline panel, NOT competing BottomSheet ── */
  .presentation-content {
    max-height: 50vh;
    overflow-y: auto;
    -webkit-overflow-scrolling: touch;
  }

  .presentation-media-grid {
    display: grid !important;
    grid-template-columns: repeat(auto-fill, minmax(100px, 1fr)) !important;
    gap: 8px !important;
    padding: 8px !important;
  }

  .presentation-media-item {
    border-radius: 8px;
    overflow: hidden;
  }

  .presentation-media-item img {
    width: 100%;
    height: auto;
    display: block;
  }

  /* ==========================================================================
     crisp-anchoring-cradle S1 D1-C — Iron-Law fix for Media horizontal mis-
     anchor (Issue #6, surfaced 2026-05-11 /listen). styles.css:37845-37854
     overrides #view-media with a desktop-only negative-margin trick:

       #view-media {
         margin: calc(-1 * var(--space-xl));
         width: calc(100% + 2 * var(--space-xl));
       }

     That negates the App-Panel padding so the desktop Media Curator fills the
     panel exactly. On mobile, .main-content's padding is already env-safe-
     area only (no --space-xl floor), so the desktop's negative-margin shifts
     #view-media LEFT by --space-xl (~24px) — clipping the .mobile-library
     header's "Media" → "Mledia" and "17,620" → "7,620" off the iPhone left
     edge. The Iron-Law root-cause fix is to override the desktop margin/width
     at the mobile breakpoint so #view-media (and the other panel-fill views)
     align flush to the viewport's safe-area-padded left edge. */
  #view-media,
  #view-about,
  #view-story-presentation {
    margin: 0 !important;
    width: 100% !important;
    /* mending-iphone-debut S1 Issue #5 — Iron-Law cascade-default fix for
       Media vertical scroll. Desktop styles.css:37874-37883 sets
       `#view-media { height: calc(100vh - var(--nav-height)); overflow: hidden }`
       so the desktop Media Curator fills the App Panel exactly. The prior
       mobile override covered margin + width but inherited the desktop
       height + overflow, clipping .mobile-library at viewport height —
       ml-grid content beyond the first screen was hidden behind the clip
       and the user could not scroll past initial rows.
       .main-content (mobile.css:647-658, position:fixed + overflow-y:auto)
       is the canonical mobile scroll surface; its children must let content
       drive their own height and pass overflow through. Same cascade-default
       smoking gun pattern as the top-nav-white incident (LESSONS §187 +
       feedback_top_nav_white_smoking_gun): every property the mobile
       surface inherits from a desktop cascade-default must be explicitly
       overridden, not just the ones that look obviously wrong. */
    height: auto !important;
    overflow: visible !important;
  }

  /* ==========================================================================
     crisp-anchoring-cradle S1 D1-D — UMI Chrome Contract: full-vertical
     between top band and bottom band. The .msp-root scaffold (defined in
     mobile-sheet-3panel.css) is `position: fixed; inset: 0` — fills the full
     viewport including under the global .top-nav (which sits at z:100 with
     opaque white) and under the .mobile-tab-bar (z:11000 above msp-root's
     9000). Joe's Issue #7 brief: UMI must occupy the full vertical space
     between the global top band and bottom band. Anchoring msp-root between
     --mobile-top-bar-total-height (from top) and --mobile-tab-bar-total-
     height (from bottom) gives that "between the bands" placement while
     preserving the global chrome's persistent visibility per Joe's mobile
     navigation directive (every modal exposes hamburger + title + Fergi AI
     up top and the tab bar below). The `.msp-root.msp-over-story` story-
     presentation variant intentionally keeps its 8vh top inset (sheet
     metaphor over a playing story) — only the standalone UMI mode adopts the
     band-to-band geometry. */
  .msp-root:not(.msp-over-story) {
    top: var(--mobile-top-bar-total-height) !important;
    bottom: var(--mobile-tab-bar-total-height) !important;
  }
  .msp-root:not(.msp-over-story) .msp-frame {
    padding-top: 0;        /* safe-area already accounted for via .top-nav band */
    padding-bottom: 0;     /* safe-area already accounted for via .mobile-tab-bar band */
  }

  /* ==========================================================================
     Phase 7: Navigation Transitions
     ========================================================================== */

  /* ── Slide-in from right (forward navigation) ── */
  @keyframes mobileViewSlideIn {
    from {
      opacity: 0;
      transform: translateX(30px);
    }
    to {
      opacity: 1;
      transform: translateX(0);
    }
  }

  /* ── Slide-in from left (back navigation) ── */
  @keyframes mobileViewSlideBack {
    from {
      opacity: 0;
      transform: translateX(-30px);
    }
    to {
      opacity: 1;
      transform: translateX(0);
    }
  }

  .view.mobile-view-slide-in {
    animation: mobileViewSlideIn 0.28s cubic-bezier(0.32, 0.72, 0, 1) both;
  }

  .view.mobile-view-slide-back {
    animation: mobileViewSlideBack 0.28s cubic-bezier(0.32, 0.72, 0, 1) both;
  }

}

/* Desktop: ensure mobile-only elements are hidden */
@media (min-width: 768px) {
  .mobile-view-title,
  .mobile-context-dot {
    display: none !important;
  }
}
