/* ============================================================================
   mobile-polish.css  ·  Additive mobile performance & flow polish
   ----------------------------------------------------------------------------
   Loaded last (after app.css / app-2.css) so it wins specificity ties without
   editing any existing rule. Every block here is PURELY ADDITIVE: it only sets
   properties that the target selectors do not already declare (touch-action,
   content-visibility, overscroll-behavior, will-change, tap-highlight), plus a
   few clearly-beneficial mobile-scoped touch-target and reduced-motion tweaks.
   Nothing here changes desktop layout or existing look.

   Scope covers the five requested surfaces:
     · My Stances        (#my-stances / .ms-*)
     · Alignment Tool    (.align-*)
     · Stance Library    (#stance-library / .sl-*)
     · Comparisons       (.cmp-*)
     · Homepage / global (nav, .nav-link, .btn-tap, .li-card, .vh-*)

   Goals: faster paint, 60fps scrolling, less jank, bigger touch targets,
   seamless transitions.
   ========================================================================== */


/* ── 1 · Tap responsiveness ──────────────────────────────────────────────
   `touch-action: manipulation` drops the ~300ms double-tap-zoom wait so taps
   register immediately, and removing the grey tap-flash makes buttons feel
   native. Harmless on desktop; every interactive control across the five
   surfaces opts in. */
.ms-chip, .ms-fchip, .ms-pos, .ms-group-head, .ms-notebtn, .ms-remove,
.ms-sum-exp, .ms-pow-btn, .ms-toggle, .ms-sh-btn, .ms-sh-ic, .ms-ov-cta-btn,
.ms-prio select,
.align-cat-head, .align-chip, .align-int-btn, .align-quick-chip,
.align-tool-btn, .align-area-chip, .align-fab, .align-compact-btn,
.align-done-btn, .align-collapse-link,
.sl-chip, .sl-card, .sl-jchip, .sl-back, .sl-btn, .sl-adopt-btn,
.sl-pol-act, .sl-search-clear, .sl-thread, .sl-rec-btn,
.cmp-empty-cta, .cmp-issue-allev, .cmp-vs-link,
.nav-link, .nav-mandate-btn, .btn-tap, .vh-cta-primary, .vh-step-cta,
.li-card,
/* Politician profile: the drawer toggles, the jump rail's pills and the pledge
   count chips are the controls a reader taps most on a phone, and none of them
   had opted in — every tap paid the ~300ms double-tap-zoom wait, which reads as
   the profile being slow rather than the browser being cautious.

   The profile's other two disclosure mechanisms are here for the same reason:
   .pdxg-toggle folds the coverage gaps, and the <summary> elements open an
   Official Record issue row, an awaiting-position list and a Flashpoint card
   body. Same job as .dd-toggle-btn, same tap, so the same treatment — a reader
   opening three sections in a row should not feel one of them lag. */
.dd-toggle-btn, .pdx-pnav-pill, .vbadge-click, .pdx-ft-rate-how, .pdx-ft-rate-sum,
.pdxg-toggle, .pdxor-row-sum, .pdxor-awaiting-d > summary, .pdx-ctv-more > summary {
  touch-action: manipulation;
  -webkit-tap-highlight-color: transparent;
}


/* ── 2 · Faster first paint — skip rendering off-screen list items ────────
   `content-visibility: auto` lets the engine skip layout & paint for cards
   that are scrolled out of view, so long stance/library/comparison lists
   paint their first screen far sooner and scroll cheaper. `contain-intrinsic-
   size` reserves a remembered placeholder height so the scrollbar never
   jumps, and items still render on demand as they approach the viewport
   (in-page scrollIntoView / find-in-page keep working). Scoped to phones,
   where these lists are single-column and paint cost hurts most. */
@media (max-width: 640px) {
  .ms-issue {
    content-visibility: auto;
    contain-intrinsic-size: auto 128px;
  }
  .sl-card {
    content-visibility: auto;
    contain-intrinsic-size: auto 190px;
  }
  .sl-pol {
    content-visibility: auto;
    contain-intrinsic-size: auto 240px;
  }
  .cmp-issue {
    content-visibility: auto;
    contain-intrinsic-size: auto 150px;
  }
  /* ── Profile modal sections ──────────────────────────────────────────────
     The politician profile is the longest single view in the app — a dozen-plus
     `.modal-section` blocks in one scroller — and it was the one long-list
     surface this pass skipped, so the whole thing laid out and painted before
     the first screen appeared. Lazy-painting the sections is the single biggest
     win available here.

     `content-visibility: auto` implies `contain: layout style paint`, which does
     three things that can bite, so the exclusion list below is the "where safe"
     part and each entry is there for a measured reason:

       canvas             Chart.js sizes to its parent at draw time. A canvas in a
                          skipped subtree measures 0×0 and stays broken after the
                          section reveals.
       .dd-body           _pdxNavJump reads getBoundingClientRect() on a jump
                          target that can sit inside a drawer, and a skipped
                          subtree reports zeros. Cheap to exclude: a closed
                          .dd-body is not painted anyway, so little was saved.
       #pdx-promise-filter,
       .mandate-jump      position: sticky descendants — paint containment clips
                          them to the section box, so a bar meant to stick to the
                          scroller would scroll away with its section instead.
       .pdx-sib-grid      its own horizontal scroller, with sticky header cells;
                          same clipping problem, plus its width is not derivable
                          from an intrinsic-size guess.
       .pdxsp-dw          the spine's drawer-wrapper sections, per .dd-body above.

     Scoped to #modal-body so nothing outside the profile is affected, and to
     phones, where the paint cost actually hurts. `contain-intrinsic-size: auto`
     keeps each section's real measured height once it has rendered, so the
     scrollbar settles and the scroll-spy reads true positions after the first
     pass; the 320px figure is only the first-paint placeholder. Sections keep
     their own boxes while skipped, which is what the jump rail and the spy
     measure, and find-in-page still reveals skipped content.

     An engine without :has() support drops the whole rule, which is exactly the
     pre-existing behaviour — so this degrades to a no-op rather than to a bug. */
  #modal-body .modal-section:not(.pdxsp-dw):not(:has(canvas, .dd-body, #pdx-promise-filter, .mandate-jump, .pdx-sib-grid)) {
    content-visibility: auto;
    contain-intrinsic-size: auto 320px;
  }
}
@media (max-width: 560px) {
  /* Local Issues collapses to one column at 560px — safe to lazy-paint here. */
  #local-issues .li-card {
    content-visibility: auto;
    contain-intrinsic-size: auto 220px;
  }
}


/* ── 3 · Smoother scrolling — momentum + contained overscroll ─────────────
   Momentum (inertial) scrolling on iOS for the scrollable panels, and
   `overscroll-behavior: contain` so a swipe that reaches the end of a chip
   rail or a modal doesn't chain into the page behind it or trigger the
   browser's back-swipe. */
.sl-chips {
  -webkit-overflow-scrolling: touch;
  overscroll-behavior-x: contain;
}
.ms-ov-panel {
  -webkit-overflow-scrolling: touch;
  overscroll-behavior: contain;
}
/* The profile's sticky jump rail is a horizontal scroller like .sl-chips above.
   Without containment, swiping it past either end chains into the modal body — or
   into the browser's back-swipe gesture, which closes the profile mid-read. */
.pdx-pnav-track {
  overscroll-behavior-x: contain;
}

/* Land smooth-scroll / anchor jumps just below the fixed top nav instead of
   underneath it — makes in-app navigation feel intentional, not clipped.
   Superseded at every width by the --pdx-nav-h rule in section 7c below, which
   derives the same offset from the nav's measured height; kept as the value a
   phone falls back to if that custom property is ever unresolvable. */
@media (max-width: 640px) {
  html { scroll-padding-top: 72px; }
}


/* ── 4 · Seamless transitions — promote only what actually animates ───────
   A light, targeted `will-change` on the handful of elements that transform
   or fade (never a blanket promotion) hands them to the compositor so their
   transitions stay on the 60fps path. All are single, small elements. */
.align-fab { will-change: transform; }
.ms-toast { will-change: transform, opacity; }
.ms-ov-panel { will-change: transform; }
.ms-group-caret, .align-cat-chev { will-change: transform; }

/* The fixed, backdrop-blurred top nav repaints as the page scrolls under it;
   pinning it to its own compositor layer keeps that repaint off the main
   scroll frame. */
nav.nav-blur {
  will-change: transform;
  transform: translateZ(0);
}

/* Card hover-lift transitions are pointer-only flourishes; on touch devices
   they can't fire but the compositor still watches for them. Neutralize the
   transition cost on coarse pointers so first paint and scroll are leaner. */
@media (hover: none) and (pointer: coarse) {
  .sl-card, .li-card, .ms-chip { transition: none; }
}


/* ── 5 · Comfortable touch targets (≥44px) ───────────────────────────────
   Several controls render below the 44px tap minimum on phones. Grow them on
   coarse-pointer / small screens only, leaving the compact desktop look
   untouched. Uses min-height + centering so nothing reflows awkwardly. */
@media (hover: none) and (pointer: coarse) {
  .cmp-vs-link {
    min-width: 40px;
    min-height: 40px;
  }
  .cmp-chip,
  .cmp-empty-cta {
    min-height: 34px;
    display: inline-flex;
    align-items: center;
  }
  .sl-jchip,
  .sl-chip,
  .sl-rec-btn,
  .sl-pol-act {
    min-height: 40px;
    display: inline-flex;
    align-items: center;
  }
  .sl-back,
  .sl-search-clear {
    min-width: 40px;
    min-height: 40px;
    display: inline-flex;
    align-items: center;
    justify-content: center;
  }
  .ms-chip,
  .ms-fchip,
  .ms-sh-ic {
    min-height: 40px;
    display: inline-flex;
    align-items: center;
  }
}


/* ── 6 · Battery & jank — hush decorative loops for reduced-motion ────────
   Additive top-up to the existing prefers-reduced-motion handling: silence the
   remaining infinite/decorative loops on the polished surfaces so low-power
   phones aren't animating off-screen. Layout and interactivity are unchanged. */
@media (prefers-reduced-motion: reduce) {
  .align-fab.is-empty,
  .align-panel::before,
  .ms-flash,
  .align-flash {
    animation: none !important;
  }
  .align-chip.just-toggled,
  .align-cat-badge.pop,
  .align-sel-count.pop {
    animation: none !important;
  }
}


/* ══════════════════════════════════════════════════════════════════════════
   7 · LAYOUT STABILITY — app-shell hardening
   --------------------------------------------------------------------------
   Everything below targets site-wide "jumpiness": content that moves after you
   have started reading it. It pairs with pdx-stability.js, which owns the
   scroll-lock coordinator, the Safari scroll-anchoring fallback and the live
   --pdx-nav-h measurement that these rules consume. Still purely additive: no
   existing declaration is replaced, and every layout-affecting change is either
   mobile-scoped or a no-op where the browser already behaves.
   ══════════════════════════════════════════════════════════════════════════ */

/* ── 7a · Reserve the scrollbar's space, permanently ──────────────────────
   ~40 call sites across 15 feature files lock the page by setting
   `overflow: hidden` on <body> when a modal opens. On any desktop/laptop with
   classic (space-taking) scrollbars, that removes the scrollbar and the whole
   document instantly re-lays-out ~15px wider — then snaps back on close. Every
   modal open/close was a full-page horizontal reflow. Reserving the gutter for
   good makes the lock free. Phones use overlay scrollbars, so this is inert
   there; declared unscoped so the desktop case is fixed too. */
html {
  scrollbar-gutter: stable;
}

/* ── 7b · The scroll lock, done once and done properly ────────────────────
   pdx-stability.js adds this class to <html> for the duration of any modal.
   Two things the per-file `body { overflow: hidden }` could not do:
     · iOS Safari ignores overflow on <body> for the document scroller but
       honours it on <html>, so the page behind an open profile actually stays
       put instead of drifting while you read the modal.
     · `scroll-behavior: auto` keeps the position restore on close instant —
       the global `html { scroll-behavior: smooth }` would otherwise animate a
       correction that is meant to be invisible.
   Modal internals are `position: fixed` with their own overflow, so their
   scrolling is unaffected. */
html.pdx-scroll-locked {
  overflow: hidden;
  scroll-behavior: auto;
}
/* Applied for the single frame around any internal scroll correction, so
   engines that ignore `behavior: 'instant'` still land instantly. */
html.pdx-instant-scroll {
  scroll-behavior: auto !important;
}

/* ── 7c · One source of truth for the fixed nav's height ───────────────────
   Sticky jump-bars and scroll padding hard-coded the nav height (62px, 80px,
   108px) at a single breakpoint, so on phones the sticky bars tucked under the
   nav and hash jumps landed with the target heading hidden behind it.
   pdx-stability.js measures the nav (including after the late webfont swap,
   which changes its text metrics) and overwrites --pdx-nav-h; the value here is
   the correct fallback for the mobile bar if JS never runs. */
:root {
  --pdx-nav-h: 57px;
}

/* Land every hash jump / scrollIntoView clear of the nav, at every width.
   Unscoped so desktop stops landing 108px low as well. */
html {
  scroll-padding-top: calc(var(--pdx-nav-h) + 16px);
}

/* Sections that never got an inline scroll-margin-top now get one. Inline
   styles still win where a section already tuned its own value, so this only
   fills the gaps (#hot-topics, #digital-library, #say-vs-do, #trending-reforms,
   #compare-hub, #agenda, …). */
section[id] {
  scroll-margin-top: calc(var(--pdx-nav-h) + 16px);
}

/* The two sticky quick-jump rails both hard-coded `top: 62px`. Pin them to the
   measured nav instead so they sit flush under it rather than overlapping it or
   floating clear of it. */
.el-jump,
.mandate-jump {
  top: calc(var(--pdx-nav-h) + 5px);
}

/* ── 7d · Stop the sticky chrome repainting the page behind it, per frame ──
   `backdrop-filter: blur(…)` on a full-width fixed/sticky bar forces the
   compositor to re-sample and re-blur everything underneath it on EVERY scroll
   frame. On mid-range phones that is the single most expensive thing on the
   page and it reads as stuttering, unstable scrolling. The bars are already
   ~80–92% opaque, so on phones we drop the blur and top the background up to
   fully opaque: near-identical appearance, none of the per-frame cost. Desktop
   keeps the blur untouched. */
@media (max-width: 640px) {
  nav.nav-blur {
    -webkit-backdrop-filter: none;
    backdrop-filter: none;
    background-color: #0a0f1e;
  }
  .el-jump,
  .mandate-jump {
    -webkit-backdrop-filter: none;
    backdrop-filter: none;
  }
}

/* ── 7e · Hush the huge decorative blur loops on phones ───────────────────
   The hero and several section backgrounds stack 900px-wide radial glows behind
   a `blur(120–160px)` filter and animate them on infinite loops. Each frame
   re-rasterises a very large blurred layer, which competes with scrolling on
   exactly the devices that can least afford it. On phones the glows stay — they
   are part of the look — but they stop animating, so they rasterise once.
   Desktop is untouched. */
@media (max-width: 640px) {
  .animate-pulse2,
  .animate-float {
    animation: none !important;
  }
}

/* ── 7f · Async-loaded sections must not collapse to zero ─────────────────
   #say-vs-do, #issue-front-door and #hr1-showcase ship as empty, `hidden`
   stubs and are filled in later by their feature modules; #local-issues and
   the admin sections toggle `display` from JS. Each of those transitions moves
   everything below it. pdx-stability.js absorbs the shift on engines without
   native scroll anchoring; this reserves a floor for the fade-in so the growth
   that remains is a small settle rather than a jump from nothing. Scoped to
   phones and to the populated state — an empty stub still takes no space, so a
   self-gating section that has no data to show never leaves a blank band. */
@media (max-width: 640px) {
  #say-vs-do:not(:empty):not([hidden]),
  #issue-front-door:not(:empty):not([hidden]),
  #hr1-showcase:not(:empty):not([hidden]) {
    min-height: 320px;
  }
}

