/**
 * Shell V2 — app grid, top bar, bottom tabs, safe-areas.
 *
 * Scoped: activates only on <body class="app-shell">. Legacy pages unaffected.
 * Tokens: canonical --t-* only. No hex, no legacy aliases.
 *
 * ─── Shell-private runtime vars (ADR-0002) ──────────────────────────────
 * These are LAYOUT RUNTIME vars, not design tokens. They do NOT belong in
 * inyourbody-design-system/project/colors_and_type.css. Do not add them to
 * the brand token catalog — they only make sense inside Shell V2.
 *
 *   --app-topbar-h   Reserved height of the floating top pill (incl. margins).
 *                    Set here on body.app-shell. Read by .app-screen sizing
 *                    math and any consumer that needs to offset from the top
 *                    chrome.
 *
 *   --app-tabs-h     Reserved height of the floating bottom dock (incl. gap
 *                    + safe-area slack). Set here on body.app-shell. Read
 *                    by .app-content's bottom padding, .app-sticky-cta's
 *                    sticky offset, and .app-scrim--bottom's height.
 *
 *   --kb-inset       Live keyboard inset in px. Set at runtime by
 *                    assets/js/ui.js via window.visualViewport.resize → the
 *                    JS writes the current inset to body.style. Read by
 *                    .app-content padding-bottom and .app-sticky-cta offset
 *                    so the CTA rides above the soft keyboard on iOS/Android.
 *
 * Why not brand tokens: values depend on device chrome, PWA mode, and live
 * keyboard state — they change at runtime. Brand tokens are static visual
 * constants. Keeping these shell-private preserves a clean boundary between
 * design system (colors/type/shape/shadow/motion) and app layout runtime.
 */
/* Shell V2 scroll invariant: html/body must never become the vertical
   scroller on app-shell pages. Keep all page scrolling inside .app-content.
   Use stable 100svh here; 100dvh follows iOS browser chrome changes and can
   shift fixed shell UI such as the topbar and bottom dock. */
html:has(> body.app-shell) {
  height: 100%;
  overflow: hidden;
  background: var(--t-shell-bg, var(--t-bg));
}

:root {
  /* Shared mobile bottom-surface contract. Keep the surface inside the
     viewport; iOS clips/paints over fixed chrome that is pushed below it. */
  --mobile-shell-dock-bottom: 20px;
  --mobile-shell-bottom-underlay-z: 10;
  --mobile-shell-dock-z: 1500;
  --mobile-shell-workout-dock-z: 2600;
  --mobile-shell-bottom-safe-pad: env(safe-area-inset-bottom, 0px);
  --mobile-shell-dock-pad-x: 6px;
  --mobile-shell-dock-pad-top: 6px;
  --mobile-shell-dock-pad-bottom: 6px;
  --mobile-shell-dock-item-gap: 2px;
  --mobile-shell-dock-icon-w: 40px;
  --mobile-shell-dock-icon-h: 28px;
  --mobile-shell-dock-icon-size: 24px;
  --mobile-shell-dock-label-size: 11px;
  --mobile-shell-dock-label-line-height: 1;
  /* Actual floating dock height: outer padding + item vertical stack
     (7px + icon 28px + gap 2px + label 11px + 6px) + 1px borders. */
  --mobile-shell-dock-h: calc(
    var(--mobile-shell-dock-pad-top)
    + 54px
    + var(--mobile-shell-dock-pad-bottom)
    + 2px
  );
}

body.app-shell {
  margin: 0;
  min-height: 100vh; /* fallback */
  min-height: 100svh;
  height: 100vh; /* fallback */
  height: 100svh;
  min-height: 100vh;
  overflow: hidden;
  overscroll-behavior: none;
  background: var(--t-shell-bg, var(--t-bg));
  color: var(--t-text);
  font-family: var(--t-font);

  /* keyboard-inset (set by assets/js/ui.js on focus/resize) */
  --kb-inset: 0px;
  --ios-browser-scroll-slack: 0px;

  /* Shared mobile shell geometry. Admin mobile maps its --amo-* vars to the
     same contract in admin/mobile-shell.css. */
  --mobile-shell-topbar-h: 68px;
  --mobile-shell-tabs-h: calc(var(--mobile-shell-dock-bottom) + var(--mobile-shell-dock-h));
  --app-topbar-h: var(--mobile-shell-topbar-h);
  --app-tabs-h: var(--mobile-shell-tabs-h);
  /* Legacy bottom spacing used by message screens. Do not use for shell CTA
     geometry: the sticky CTA has its own explicit clearance below. */
  --cta-dock-gap: 30px;
  /* Visible gap between sticky CTA bottom and the dock's top edge. */
  --cta-dock-clearance: 4px;
  /* Scroll reserve for screens with a fixed terminal CTA: keeps the last
     content row liftable above the action lane on short viewports. */
  --app-sticky-cta-h: 56px;
  --app-sticky-cta-scroll-gap: 112px;

  /* Single app-frame width (ADR: client UX mobile-first at every viewport).
     Content column + fixed chrome pills (topbar / tabs / sticky CTA) share
     this width ≥640px. Split into w / half / pad so the frame-edge calc
     stays flat — no division inside calc() (Safari/iOS safer). */
  --app-frame-w: 560px;
  --app-frame-half: 280px;
  --app-frame-pad: 14px;

  display: flex;
  flex-direction: column;
  padding-top: env(safe-area-inset-top, 0);
}

/* App shell is a tappable-surface environment, not a webpage — kill the
   inherited a:hover underline from style.css. Keep focus-visible ring
   for a11y. Prose links inside .prose re-enable underline below. */
body.app-shell a,
body.app-shell a:hover,
body.app-shell a:focus {
  text-decoration: none;
}
body.app-shell a:focus-visible,
body.app-shell button:focus-visible {
  outline: 2px solid var(--t-primary);
  outline-offset: 3px;
  border-radius: 10px;
}
body.app-shell .prose a,
body.app-shell .prose a:hover {
  text-decoration: underline;
}

/* ── Top app bar — floating liquid-glass pill ────────────────────────────
   Shell chrome: position: fixed to the viewport so content scrolls UNDER
   the pill (signature iOS-26 backdrop-filter dissolve). Matched pair with
   the bottom dock — both floating pills owned by the shell, not the page.
   Horizontal insets match the dock (14px) so the two chrome pills bookend
   the viewport, not the content column. */

.app-topbar {
  position: fixed;
  top: calc(env(safe-area-inset-top, 0) + 10px);
  left: 14px;
  right: 14px;
  z-index: 40;
  display: flex;
  align-items: center;
  gap: 8px;
  margin: 0;
  min-height: 48px;
  padding: 8px 14px;
  border-radius: 22px;
  border: 1px solid var(--t-topdock-border);
  background: var(--t-topdock-bg);
  -webkit-backdrop-filter: blur(22px) saturate(1.7);
  backdrop-filter: blur(22px) saturate(1.7);
  box-shadow: var(--t-topdock-shadow);
  transition: box-shadow var(--t-motion-base, 180ms) var(--t-ease-premium, ease);
}

/* Scrolled visual state (ADR-0003). Toggled by ui.js when .app-content
   scrollTop > 4. Layers a violet-tinted lift on top of the ambient
   topdock-shadow so the floating pill visibly "lifts off" when content
   passes beneath it. Token-driven — reuses --t-primary-rgb + shell motion
   contract (--t-motion-base / --t-ease-premium). No new animation language. */
.app-topbar--scrolled {
  box-shadow: var(--t-topdock-shadow),
              0 8px 24px rgba(var(--t-primary-rgb), 0.14);
}

.app-topbar__back,
.app-topbar__trailing {
  width: 34px;
  height: 34px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  background: transparent;
  border: none;
  color: var(--t-text);
  cursor: pointer;
  border-radius: 10px;
  text-decoration: none;
  flex-shrink: 0;
}
.app-topbar__back { margin-left: -4px; }
.app-topbar__back svg,
.app-topbar__trailing svg {
  display: block;
}

.app-topbar__back:active,
.app-topbar__trailing:active {
  background: var(--t-border);
}

.app-topbar__trailing-wrap {
  display: inline-flex;
  align-items: center;
  margin-left: auto;
  flex-shrink: 0;
}

.app-topbar__notifications {
  display: inline-flex;
  align-items: center;
  gap: 2px;
}

.app-topbar__notify {
  -webkit-appearance: none;
  appearance: none;
  position: relative;
  width: 34px;
  height: 34px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  padding: 0;
  border: 0;
  border-radius: 12px;
  background: transparent;
  box-shadow: none;
  color: var(--t-text);
  cursor: default;
  -webkit-tap-highlight-color: transparent;
}

a.app-topbar__notify {
  cursor: pointer;
  text-decoration: none;
}

.app-topbar__notify svg {
  display: block;
}

.app-topbar__notify:active {
  background: transparent;
}

.app-topbar__notify:focus {
  outline: none;
}

.app-topbar__notify:focus-visible {
  outline: 2px solid color-mix(in srgb, var(--t-primary) 42%, transparent);
  outline-offset: 2px;
}

.app-topbar__notify-dot {
  position: absolute;
  top: 7px;
  right: 7px;
  width: 8px;
  height: 8px;
  border-radius: 999px;
  background: var(--t-danger);
  box-shadow: 0 0 0 2px var(--t-topdock-bg);
}

/* Compact shell brand mark — leading slot on root screens only.
   Suppressed automatically when a back button is present (top_bar.php). */
.app-topbar__brand {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 22px;
  height: 22px;
  margin-right: 10px;
  flex-shrink: 0;
}
.app-topbar__brand-img {
  width: 22px;
  height: 22px;
  display: block;
  user-select: none;
  -webkit-user-drag: none;
}
/* Theme-aware asset swap: dark mark on light bg, white mark on dark/violet bg. */
.app-topbar__brand-img--dark { display: none; }
[data-theme="dark"] .app-topbar__brand-img--light,
[data-theme="violet"] .app-topbar__brand-img--light { display: none; }
[data-theme="dark"] .app-topbar__brand-img--dark,
[data-theme="violet"] .app-topbar__brand-img--dark { display: block; }

/* Section identity icon — leading slot on root/tab screens that aren't Home.
   Replaces the compact brand mark for per-tab identity (training=barbell,
   progress=chart-line-up, food=fork-knife, profile=user-circle). Suppressed
   automatically when a back button is present (top_bar.php). */
.app-topbar__section-icon {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 22px;
  height: 22px;
  margin-right: 10px;
  color: var(--t-text);
  flex-shrink: 0;
}
.app-topbar__section-icon svg { display: block; }

/* Secondary identity mark — small icon rendered next to the title on
   sub-screens. Used for family identity (e.g. training sub-screens keep the
   dumbbell as a soft brand marker) without displacing the back arrow. */
.app-topbar__secondary {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  margin-left: 8px;
  color: var(--t-text-muted);
  opacity: 0.55;
  flex-shrink: 0;
  vertical-align: -2px;
}
.app-topbar__secondary svg { display: block; }

.app-topbar__title {
  flex: 1;
  margin: 0;
  font-size: 17px;
  font-weight: 700;
  color: var(--t-text);
  text-align: left;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

/* When a back button is present, add a little inset */
.app-topbar__back + .app-topbar__title {
  margin-left: 4px;
}

/* Profile cluster (Home) — brand mark + display name + chevron as one
   tappable unit. Reads as a premium app header/title, not as a button
   or a web link. Entire cluster is the tap target. */
.app-topbar__profile {
  flex: 1;
  display: inline-flex;
  align-items: center;
  min-width: 0;
  height: 100%;
  padding: 0 6px 0 0;
  margin: 0 -6px 0 0;
  color: inherit;
  text-decoration: none;
  border-radius: 10px;
  -webkit-tap-highlight-color: transparent;
  transition: background-color 0.12s var(--t-ease-premium, ease);
}
.app-topbar__profile:active {
  background: rgba(var(--t-primary-rgb), 0.06);
}
.app-topbar__profile .app-topbar__brand {
  margin-right: 10px;
  flex-shrink: 0;
}
.app-topbar__title--profile {
  flex: 0 1 auto;
  margin: 0;
  max-width: calc(100% - 28px);
  font-size: 17px;
  font-weight: 700;
  color: var(--t-text);
  line-height: 1.1;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  letter-spacing: -0.005em;
}
.app-topbar__profile-chev {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  margin-left: 2px;
  color: var(--t-text-muted);
  opacity: 0.55;
  flex-shrink: 0;
  transition: transform 0.15s var(--t-ease-premium, ease), opacity 0.15s;
}
.app-topbar__profile-chev svg {
  display: block;
}
.app-topbar__profile:active .app-topbar__profile-chev {
  opacity: 0.85;
  transform: translateX(1px);
}

/* ── Content area ────────────────────────────────────────────────────── */

.app-content {
  flex: 1 1 auto;
  min-height: 0;
  /* WebKit flex-item sizing quirk: without an explicit width, a flex child
     that is also an overflow:auto scroll container can collapse its cross-
     axis size to intrinsic min-content instead of stretching to the parent
     (then clamped by max-width). Narrow-content pages (e.g. progress hub)
     collapsed to ~296px on desktop despite max-width: var(--app-frame-w).
     width: 100% forces a definite cross-size so max-width clamps correctly. */
  width: 100%;
  overflow-y: auto;
  overscroll-behavior: contain;
  overscroll-behavior-y: contain;
  /* Top/bottom padding reserves clearance for the fixed floating topbar and
     dock — content scrolls under both via the matching paint layers. 16px
     horizontal gutter keeps familiar content alignment. */
  padding: calc(var(--app-topbar-h) + 16px) 16px calc(var(--app-tabs-h) + env(safe-area-inset-bottom, 0) + 24px + var(--kb-inset) + var(--ios-browser-scroll-slack));
}

/* Screens with a shell-owned terminal CTA reserve extra bottom clearance so
   the last content row clears BOTH the dock and the fixed CTA action lane.
   Covers the two terminal-CTA markups in production: .app-sticky-cta (emitted
   by AppShellRenderer::close) and .vbp-start-cta (legacy hand-rolled shell
   on modules/training/program.php). */
body.app-shell:has(.app-sticky-cta) .app-content,
body.app-shell:has(.vbp-start-cta) .app-content {
  padding-bottom: calc(
    var(--app-tabs-h)
    + env(safe-area-inset-bottom, 0)
    + var(--kb-inset)
    + var(--cta-dock-clearance)
    + var(--app-sticky-cta-h)
    + var(--app-sticky-cta-scroll-gap)
    + var(--ios-browser-scroll-slack)
  );
}

@supports (-webkit-touch-callout: none) {
  @media (display-mode: browser) and (hover: none) and (pointer: coarse) {
    body.app-shell {
      --ios-browser-scroll-slack: 92px;
    }

    body.app-shell .app-content {
      -webkit-overflow-scrolling: touch;
    }
  }
}

body.app-shell:has(.msg-screen--thread) .app-content {
  overflow: hidden;
  padding-bottom: calc(
    var(--msg-composer-h, var(--app-sticky-cta-h))
    + var(--mobile-shell-dock-bottom)
    + 24px
    + var(--kb-inset)
  );
}

body.app-shell:has(.msg-screen--thread) .app-screen,
body.app-shell:has(.msg-screen--thread) .app-screen__body {
  height: 100%;
  min-height: 0;
}

/* .app-screen / .app-screen__body are emitted by AppShellRenderer as plain
   block wrappers. The flex-col stage from v2.136.0 was rolled back in this
   wave because the CTA is now shell chrome (position: fixed), not a flex
   slot inside content. The wrappers remain in markup to preserve the class
   contract but have no styling — block-flow passthrough. */

/* ── Shell-owned bottom action lane (ADR-0003 v2) ─────────────────────────
   Both .app-sticky-cta (AppShellRenderer-emitted) and .vbp-start-cta (legacy
   hand-rolled shell on program.php) share the same chrome contract: fixed
   to the viewport above the dock's reserved zone, with matching horizontal
   insets. The wrapper is transparent (scrim owns the fade) and pointer-
   events:none so taps in surrounding padding pass to content; the CTA
   element inside opts back in via > *. Unified here so every terminal CTA
   across the app uses the exact same action lane geometry. */

body.app-shell .app-sticky-cta,
body.app-shell .vbp-start-cta {
  position: fixed;
  left: 14px;
  right: 14px;
  /* Park CTA from the dock's real geometry: dock bottom offset + dock height
     (`--app-tabs-h`) + explicit visual clearance. This avoids relying on
     safe-area as an accidental Android/iPhone offset. */
  bottom: calc(var(--app-tabs-h) + var(--cta-dock-clearance) + var(--kb-inset));
  z-index: 35;
  margin: 0;
  padding: 0;
  background: none;
  pointer-events: none;
}

body.app-shell .app-sticky-cta > *,
body.app-shell .vbp-start-cta > * {
  pointer-events: auto;
}

/* Messages are a focused chat surface: the composer/new-topic action replaces
   the bottom dock instead of floating above it. */
body.app-shell:has(.msg-screen) .app-sticky-cta {
  left: 12px;
  right: 12px;
  bottom: var(--mobile-shell-dock-bottom);
  z-index: var(--mobile-shell-dock-z);
}

/* CTA-lane underlay (ADR-0003 v3). Solid shell-bg fill extending from the
   CTA wrapper's bottom edge down past the dock's top edge. Fills the 6-10px
   strip between the yellow pill and the dock that the bottom-scrim gradient
   only fades over (scrim solid-stop at 45% = ~63px from viewport bottom;
   dock top ~82-92px from bottom). Without this fill, scrolling content
   flashes through that strip during fast scroll and iOS address-bar
   transitions — read as CTA "flying away" from the dock. Dock z:50 paints
   above the underlay so the fill only shows in the exposed strip — no new
   visual surface, just continues the shell-bg canvas under the action lane. */
body.app-shell .app-sticky-cta::after,
body.app-shell .vbp-start-cta::after {
  content: '';
  position: absolute;
  left: -14px;
  right: -14px;
  top: 100%;
  height: calc(var(--app-tabs-h) + env(safe-area-inset-bottom, 0));
  background: var(--t-shell-bg, var(--t-bg));
  pointer-events: none;
  z-index: -1;
}

body.app-shell--focus .app-sticky-cta,
body.app-shell--focus .vbp-start-cta {
  bottom: calc(env(safe-area-inset-bottom, 0) + var(--kb-inset) + 12px);
}

body.app-shell--keyboard .app-sticky-cta {
  bottom: calc(env(safe-area-inset-bottom, 0) + var(--kb-inset) + 12px);
}

body.app-shell--keyboard .app-topbar {
  transform: translateY(var(--vv-offset-top, 0px));
}

body.app-shell--keyboard:has(.msg-screen) .app-sticky-cta {
  bottom: max(6px, calc(var(--kb-inset) - env(safe-area-inset-bottom, 0px) + 6px));
}

body.app-shell--focus .app-sticky-cta::after,
body.app-shell--focus .vbp-start-cta::after { display: none; }

/* ── Bottom scrim (ADR-0003) ──────────────────────────────────────────────
   Shell-owned fade layer between content (z: default) and the sticky CTA
   (z: 30), below the dock (z: 50). Always rendered on mobile non-focus; CSS
   hides it in focus mode (no dock to fade into) and on desktop ≥900px
   (left-rail replaces the floating dock). pointer-events: none so taps
   pass through to content below. */

.app-scrim--bottom {
  position: fixed;
  left: 0;
  right: 0;
  bottom: 0;
  height: calc(var(--app-tabs-h) + env(safe-area-inset-bottom, 0) + 48px);
  pointer-events: none;
  z-index: var(--mobile-shell-bottom-underlay-z);
  background: linear-gradient(to top, var(--t-shell-bg, var(--t-bg)) 45%, transparent);
}

body.app-shell--focus .app-scrim--bottom { display: none; }

/* ── Top scrim (ADR-0003 — top pair) ──────────────────────────────────────
   Shell-owned fade layer between content (z: default) and the floating
   topbar (z: 40). Purpose: content visually dissolves under the topbar
   instead of hitting it as a hard edge. Denser near the top, fades to
   transparent downward — a soft support for the pill, not a second surface.
   pointer-events: none so taps pass through. Stays visible in focus mode
   (topbar is NOT hidden in focus — only the bottom tabs are — so the top
   scrim must keep supporting it, otherwise content shows above the pill on
   scroll during active workout and any other focus-mode screen). Hidden on
   desktop ≥900px (topbar sits as a sticky card inside the centered column,
   no floating chrome to dissolve under). */

/* ── Top chrome lane — shell root ownership ──────────────────────────────
   The opaque shell-bg band lives as a pseudo-element on body.app-shell —
   the shell root itself — so it cannot be trapped by any content-subtree
   stacking/paint context (e.g. .app-content's overflow scroll container,
   the pill's backdrop-filter compositor). Previous attempts placed a
   <div class="app-top-bg"> inside <main class="app-content"> and the band
   was ineffective on iOS Safari despite being position: fixed.

   Contract: body::before owns the background fill at root. The floating
   pill (.app-topbar, z:40) and the gradient scrim (.app-scrim--top, z:30)
   composite ABOVE this band. The three together form the top chrome lane;
   content renders below. Desktop ≥900px suppresses the band alongside the
   scrim — sticky topbar in centered column, no floating chrome needed. */
body.app-shell::before {
  content: '';
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  height: calc(env(safe-area-inset-top, 0px) + 22px);
  pointer-events: none;
  z-index: 29;
  background: var(--t-shell-bg, var(--t-bg, #fafafb));
}

body.app-shell--keyboard::before {
  height: calc(env(safe-area-inset-top, 0px) + var(--vv-offset-top, 0px) + 22px);
}

.app-scrim--top {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  height: calc(env(safe-area-inset-top, 0) + var(--app-topbar-h) + 12px);
  pointer-events: none;
  z-index: 30;
  background: linear-gradient(to bottom, var(--t-shell-bg, var(--t-bg)) 45%, transparent);
}

/* ── Bottom tabs — floating liquid-glass pill ────────────────────────────
   Shared mobile dock model: lower the whole surface. Safe-area clearance
   remains in content padding, not inside the nav pill's item layout. */

.app-tabs {
  position: fixed;
  left: 12px;
  right: 12px;
  bottom: var(--mobile-shell-dock-bottom);
  z-index: var(--mobile-shell-dock-z);
  padding: var(--mobile-shell-dock-pad-top) var(--mobile-shell-dock-pad-x) var(--mobile-shell-dock-pad-bottom);
  display: grid;
  grid-template-columns: repeat(5, 1fr);
  gap: var(--mobile-shell-dock-item-gap);
  border-radius: 26px;
  border: 1px solid var(--t-dock-border);
  background: var(--t-dock-bg);
  -webkit-backdrop-filter: blur(22px) saturate(1.7);
  backdrop-filter: blur(22px) saturate(1.7);
  box-shadow: var(--t-dock-shadow);
}

body.app-shell[data-dock="floating"] .app-tabs {
  bottom: var(--mobile-shell-dock-bottom);
  left: 12px;
  right: 12px;
}

body.app-shell--focus .app-tabs {
  display: none;
}

body.app-shell--keyboard .app-tabs {
  display: none;
}

body.app-shell:has(.msg-screen) .app-tabs {
  display: none;
}

.app-tabs__item {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 2px;
  padding: 7px 4px 6px;
  border-radius: 20px;
  color: var(--t-tab-inactive-fg, var(--t-text-muted));
  text-decoration: none;
  font-size: var(--mobile-shell-dock-label-size);
  font-weight: 500;
  transition: color 0.15s, background-color 0.15s;
}

.app-tabs__item:active {
  background: rgba(var(--t-primary-rgb), 0.06);
}

.app-tabs__item--active {
  color: var(--t-tab-active-bg);
  font-weight: 700;
}
.app-tabs__item--active .app-tabs__icon {
  background: var(--t-tab-active-bg);
  color: var(--t-tab-active-fg);
  box-shadow: var(--t-tab-active-glow), inset 0 1px 0 rgba(255,255,255,0.18);
}

.app-tabs__icon {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: var(--mobile-shell-dock-icon-w);
  height: var(--mobile-shell-dock-icon-h);
  border-radius: 10px;
  line-height: 1;
  transition: background-color 0.15s, color 0.15s, box-shadow 0.15s;
}
.app-tabs__icon svg {
  display: block;
  width: var(--mobile-shell-dock-icon-size);
  height: var(--mobile-shell-dock-icon-size);
}

.app-tabs__label {
  line-height: var(--mobile-shell-dock-label-line-height);
}

/* ── Offline banner (mounted by ui.js under top bar) ─────────────────── */

.app-offline {
  padding: 8px 16px;
  font-size: 13px;
  text-align: center;
  background: var(--t-surface);
  color: var(--t-text-muted);
  border-bottom: 1px solid var(--t-border);
}

/* ── Wide-viewport centering ──────────────────────────────────────────────
   Client-facing UX is mobile-first at every viewport (ADR decisions.md).
   On wide screens we keep the same mobile app shell — floating topbar +
   floating bottom dock — and only cap the content column + pills so the
   whole app-frame stays aligned. Covers both .app-content (AppShellRenderer)
   and known hand-rolled page wrappers that predate it. Scrim, top-band
   (body.app-shell::before) and CTA underlay (::after) stay edge-to-edge —
   they're canvas fades, not frame chrome. Admin/coach pages don't use
   body.app-shell and are unaffected. */
@media (min-width: 640px) {
  body.app-shell .app-content,
  body.app-shell .home-v1,
  body.app-shell .vbp-page,
  body.app-shell .container,
  body.app-shell .msg-wrap,
  body.app-shell .pf-container,
  body.app-shell .stk-wrap {
    max-width: var(--app-frame-w);
    margin-inline: auto;
  }

  /* Centered-frame edge: on viewports narrower than the frame, pills stay
     at the mobile 14px inset; on wider viewports they align with the
     centered content column (+14px inner gutter to match the mobile look). */
  body.app-shell {
    --app-frame-edge: max(
      var(--app-frame-pad),
      calc(50vw - var(--app-frame-half) + var(--app-frame-pad))
    );
  }

  body.app-shell .app-topbar,
  body.app-shell .app-tabs,
  body.app-shell[data-dock="floating"] .app-tabs,
  body.app-shell .app-sticky-cta,
  body.app-shell .vbp-start-cta {
    left: var(--app-frame-edge);
    right: var(--app-frame-edge);
  }

  body.app-shell:has(.msg-screen) .app-sticky-cta {
    left: var(--app-frame-edge);
    right: var(--app-frame-edge);
  }
}

.app-main {
  display: flex;
  flex-direction: column;
  flex: 1 1 auto;
  min-height: 0;
  overflow: hidden;
}
