
      :root {
        --bg: #F2F2F2;
        --tp: #1A1A1A;
        --ts: #6B6B6B;
        --tm: #6E6E6E;
        --accent: #7A00E6;

        /* Motion tokens. None existed: the bundle repeats
           cubic-bezier(.16,1,.3,1) and .4,0,.2,1 inline in ~40 places. These
           name the two curves already in use plus a duration scale, so this
           pass's motion matches what is already there instead of inventing a
           third feel. Everything stays under the 320ms ceiling. */
        --ease-spring: cubic-bezier(.16, 1, .3, 1);
        --ease-standard: cubic-bezier(.4, 0, .2, 1);
        --dur-fast: 150ms;
        --dur-base: 240ms;
        --dur-slow: 320ms;
        --wave-step: 45ms;
      }
      * { box-sizing: border-box; }
      html, body { margin: 0; padding: 0; background: var(--bg); color: var(--tp); }
      body {
        font-family: system-ui, -apple-system, BlinkMacSystemFont, "Inter", "Segoe UI", "Roboto", sans-serif;
        font-feature-settings: "cv02", "cv03", "cv04", "cv11";
        text-rendering: optimizeLegibility;
        -webkit-font-smoothing: antialiased;
        -moz-osx-font-smoothing: grayscale;
        overscroll-behavior-y: contain;
        -webkit-tap-highlight-color: transparent;
      }
      #root { min-height: 100vh; min-height: 100dvh; }
      /* The week strip + primary nav now sit in a fixed bottom bar (One UI's
         bottom-weighted reach). Being fixed, they no longer occupy flow, so the
         scrolling content needs an equivalent reserve or the last card hides
         behind them. --bottom-bar-h is measured at runtime by mobile-native.js;
         the fallback only has to hold until that first measurement lands. */
      #root { padding-bottom: var(--bottom-bar-h, 132px); }
      ::-webkit-scrollbar { width: 0; height: 0; }
      div::-webkit-scrollbar { width: 6px; height: 6px; }
      div::-webkit-scrollbar-track { background: transparent; }
      div::-webkit-scrollbar-thumb { background: #CFCFCF; border-radius: 3px; }
      div::-webkit-scrollbar-thumb:hover { background: #B5B5B5; }
      button:focus-visible, input:focus-visible, textarea:focus-visible, select:focus-visible {
        outline: 2px solid var(--accent); outline-offset: 1px;
      }
      button { -webkit-user-select: none; user-select: none; }
      html, body { -webkit-overflow-scrolling: touch; }
      input::placeholder, textarea::placeholder { color: var(--tm); opacity: 1; }
      input:-webkit-autofill {
        -webkit-text-fill-color: var(--tp);
        -webkit-box-shadow: 0 0 0px 1000px #FFFFFF inset;
        transition: background-color 9999s ease-in-out 0s;
      }
      input[type="number"]::-webkit-outer-spin-button,
      input[type="number"]::-webkit-inner-spin-button { -webkit-appearance: none; margin: 0; }
      input[type="number"] { -moz-appearance: textfield; }
      body {
        padding-left: env(safe-area-inset-left);
        padding-right: env(safe-area-inset-right);
      }
      /* Status-bar clearance. The manifest asks for "standalone" (status bar
         kept), but One UI 7 / Android 15 draws installed PWAs edge-to-edge:
         the bar becomes a transparent strip OVER the page, and with
         viewport-fit=cover and no top inset the app's own header slid up
         underneath the clock - on a true-black theme the strip read as "no
         status bar at all". Reserve it. env() is 0 wherever the platform
         does not overlay (desktop, older Android), so this is a no-op there.
         The sticky app header re-pins below the bar for the same reason -
         at top:0 it would ride up under the clock again on first scroll. */
      #root { padding-top: env(safe-area-inset-top, 0px); }
      #root [style*="position: sticky"] {
        top: env(safe-area-inset-top, 0px) !important;
      }
      @keyframes fadeIn { from { opacity: 0; } to { opacity: 1; } }
      @keyframes slideUp { from { transform: translateY(20px); opacity: 0; } to { transform: translateY(0); opacity: 1; } }

      /* Classy, restrained motion — no press/tap feedback, no bounce/
         overshoot, no brightness flashes. Just a slow-settling "expensive"
         deceleration curve (the same family used by Linear/Stripe-style
         interfaces) on hover and on the handful of real transitions/
         animations elsewhere.

         The exercise expand/collapse toggle is a real <button> that ALSO
         carries inline cursor:pointer, and it — along with anything else on
         a touchscreen — was getting hit by iOS Safari's well-known "sticky
         hover" bug: a tap can trigger :hover and then never clear it,
         leaving the lift+shadow sitting there looking like a permanent
         boxed button outline after a single tap. Fixed at the root by
         requiring `(pointer: fine)` alongside `(hover: hover)` — that
         combination reliably excludes touchscreens (unlike `hover: hover`
         alone, which iOS misreports), so this motion now only ever runs
         for an actual mouse.

         SELECTOR NOTE: the space matters. The DOM serializes inline styles as
         "cursor: pointer", so the space-less `[style*="cursor:pointer"]` that
         used to be here matched NOTHING — measured 0 matches against 76
         elements whose computed cursor is pointer, i.e. desktop had no hover
         feedback at all. Both spellings are kept only as belt-and-braces. */
      [style*="cursor: pointer"],
      [style*="cursor:pointer"],
      #root button {
        transition: transform 0.2s cubic-bezier(.16,1,.3,1),
                    box-shadow 0.2s cubic-bezier(.16,1,.3,1);
      }
      @media (hover: hover) and (pointer: fine) {
        /* Card-like clickable rows get the fuller lift. */
        [style*="cursor: pointer"]:not(button):hover,
        [style*="cursor:pointer"]:not(button):hover {
          transform: translateY(-2px);
          box-shadow: 0 10px 24px rgba(0,0,0,0.16);
        }
        /* Buttons are mostly small inline controls, so they get a restrained
           version rather than the same card lift — but they DO need a hover
           cue: §3 expects one on every interactive element, and mouse users
           rely on it. */
        #root button:not(:disabled):hover {
          transform: translateY(-1px);
          box-shadow: 0 4px 12px rgba(0,0,0,0.14);
        }
      }
      /* Keyboard users get no hover cue at all, so focus must read at least as
         strongly (§8). Uses currentColor so it stays legible in both themes —
         the static --accent token is the light-theme purple and is too dark on
         the near-black dark background. */
      #root button:focus-visible,
      #root [style*="cursor: pointer"]:focus-visible {
        outline: 2px solid currentColor;
        outline-offset: 2px;
        border-radius: 8px;
      }

      /* Desktop width. Measured at 1920x1080: the app shell is capped at 480px
         and centred, so a maximised window showed a 480px column inside ~1440px
         of empty margin — §11's "stretched mobile layout" in its centred form.
         Widening the shell lets the set rows (weight / reps / RIR columns) and
         the week strip actually use the screen.
         `!important` is required: the cap is an inline style, which otherwise
         wins. A true multi-column desktop layout would need the React
         components restructured — see the findings doc. */
      @media (min-width: 1024px) {
        #root [style*="max-width: 480px"] { max-width: 720px !important; }
      }
      @media (min-width: 1440px) {
        #root [style*="max-width: 480px"] { max-width: 860px !important; }
      }

      /* Modal sheets on desktop.
         Measured at 1440x900: the menu sheet and the exercise gear popup are
         both phone bottom-sheets -- full-bleed, square top corners, pinned to
         the bottom edge -- and on desktop they rendered 1382px wide, 96% of the
         window, with the menu running to y=911 on a 900px viewport, i.e.
         clipped. That is §10's "stretched mobile layout" verbatim.

         Capped and centred instead. `margin: auto` centres a flex item on both
         axes regardless of the parent's justify-content, so the phone layout's
         flex-end pinning does not need overriding separately. Radius closes to
         all four corners because it is no longer anchored to an edge.

         The selector matches the inline style React writes. NOTE the spaces:
         the DOM serializes inline styles as "border-radius: 28px 28px 0px 0px",
         so the space-less spelling matches nothing -- the same trap that once
         left this file's hover rules dead. Both spellings kept as insurance. */
      /* Segmented unit toggle (kg | lbs | BW) overlapping the Reps field.
         Measured in the exercise gear popup at 384px: the toggle group carries
         flex-shrink: 0 and sits inside a `flex: 1 1 120px; min-width: 0`
         column, so when its natural 127px exceeded the ~106px left beside the
         weight input it did not compress -- it overflowed its column's right
         edge (156px) out to 177px and painted over the Reps input, which starts
         at 163px. The weight input was squeezed to 19px in the process.

         Trimming the segment padding takes the group to ~89px, inside the
         budget at 360px as well as 384px, and hands the reclaimed width back to
         the weight field. The wrap rule is the backstop: if a future label ever
         outgrows the column again, the toggle drops to its own line instead of
         painting over its neighbour. */
      #root div[style*="border-radius: 14px"][style*="overflow: hidden"] > button {
        padding-left: 6px !important;
        padding-right: 6px !important;
      }
      #root div[style*="flex: 1 1 120px"] > div {
        flex-wrap: wrap;
      }

      @media (min-width: 1024px) {
        #root [style*="border-radius: 28px 28px 0px 0px"],
        #root [style*="border-radius:28px 28px 0px 0px"] {
          max-width: 560px !important;
          margin: auto !important;
          border-radius: 28px !important;
          max-height: min(84vh, 760px) !important;
          overflow-y: auto !important;
        }
      }
      @keyframes modalPopBold {
        0% { transform: translateY(28px) scale(0.96); opacity: 0; }
        100% { transform: translateY(0) scale(1); opacity: 1; }
      }
      /* Touch-target floor. Measured at 412x915 with the app actually running:
         all 20 interactive controls fell under 44px on at least one axis —
         week arrows 30x34, the menu button 36x34, day buttons 40x41, the
         Train/Body/Insights tabs 123x38.

         Rather than resizing them (which would coarsen a deliberately tight
         layout, and would mean editing ~900 inline style objects inside the
         minified bundle), this expands only the HIT AREA to 44px via a
         centred pseudo-element. Visual size is untouched.

         `position: relative` here only lands on buttons that don't already set
         `position` inline — inline styles win, so anything the app positions
         itself is unaffected. Scoped to coarse pointers so mouse hit-testing
         and desktop layout are untouched. */
      @media (pointer: coarse) {
        /* The week strip scrolls horizontally, and `overflow-x: auto` forces
           the other axis to compute non-visible — so it was CLIPPING the day
           buttons' 44px hit areas vertically (measured: 8 of 20 controls still
           short). A few px of vertical padding lets them extend.
           NB the attribute value needs the space: the DOM serializes inline
           styles as "overflow-x: auto", which is why the old space-less
           selectors in day-swipe-nav.js were dead. */
        #root [style*="overflow-x: auto"] {
          padding-top: 3px;
          padding-bottom: 3px;
        }

        /* Removes any residual double-tap-zoom delay without constraining the
           scroll axis. A restrictive policy (pan-y on the page / pan-x on
           scrollers) was considered and rejected: it would trap vertical
           scrolling inside the week strip, which is a worse regression than
           the delay it would fix. */
        #root button { touch-action: manipulation; }

        /* The Account panel is built by auth-client.js and appended to
           <body>, so for six builds it sat outside this rule and kept 32px
           Sign out, 39px Change password and 41px Close on the one screen
           where a mis-tap signs you out. Same rule, same expander, no
           visual change.

           #bottombar-toggle is deliberately NOT here. It is a 56x30 tab
           pinned to the bottom edge that already overlaps the nav row above
           it; a 44px expander would reach further into MMA's button, which
           is exactly the overlap that broke Focus mode in v152. */
        #root button, #auth-panel-backdrop button { position: relative; }
        #root button::after, #auth-panel-backdrop button::after {
          content: "";
          position: absolute;
          top: 50%;
          left: 50%;
          transform: translate(-50%, -50%);
          width: 100%;
          height: 100%;
          min-width: 44px;
          min-height: 44px;
        }
      }

      /* §5 macro motion: switching top-level section slides directionally,
         keyed to nav order (Train → Body → Insights). Transform+opacity only,
         so it stays on the compositor; JS also skips it entirely under
         reduced-motion rather than relying on the block below alone. */
      @keyframes sectionSlideLeft {
        from { transform: translateX(14px); opacity: 0; }
        to   { transform: translateX(0);    opacity: 1; }
      }
      @keyframes sectionSlideRight {
        from { transform: translateX(-14px); opacity: 0; }
        to   { transform: translateX(0);     opacity: 1; }
      }
      .section-slide-left  { animation: sectionSlideLeft  0.24s cubic-bezier(.16,1,.3,1); }
      .section-slide-right { animation: sectionSlideRight 0.24s cubic-bezier(.16,1,.3,1); }

      /* Keyboard-shortcut help sheet (desktop). Lives on <body>, deliberately
         OUTSIDE #root, so mobile-native.js's layer observer does not treat it
         as an app layer and consume a system Back press for it. */
      #kbd-help {
        position: fixed;
        inset: 0;
        z-index: 9997;
        display: flex;
        align-items: center;
        justify-content: center;
        background: rgba(0,0,0,0.45);
        animation: fadeIn 0.15s ease-out;
        font-family: system-ui, -apple-system, BlinkMacSystemFont, "Inter", "Segoe UI", Roboto, sans-serif;
      }
      #kbd-help .kbd-panel {
        min-width: 300px;
        max-width: 90vw;
        padding: 20px 22px;
        border-radius: 14px;
        background: var(--bg, #1A1A1A);
        color: var(--tp, #F0F0F0);
        box-shadow: 0 18px 48px rgba(0,0,0,0.45);
        animation: sectionSlideLeft 0.2s cubic-bezier(.16,1,.3,1);
      }
      #kbd-help .kbd-title { font-size: 1rem; font-weight: 700; margin-bottom: 14px; }
      #kbd-help .kbd-row {
        display: flex; align-items: center; gap: 14px;
        margin-bottom: 8px; font-size: 0.8125rem;
      }
      #kbd-help kbd {
        flex-shrink: 0; min-width: 84px;
        padding: 3px 8px; border-radius: 6px;
        background: rgba(127,127,127,0.22);
        font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
        font-size: 0.75rem; text-align: center;
      }
      #kbd-help .kbd-close {
        margin-top: 14px; width: 100%;
        padding: 9px 0; border: none; border-radius: 9px; cursor: pointer;
        background: rgba(127,127,127,0.18); color: inherit;
        font-family: inherit; font-size: 0.8125rem; font-weight: 600;
      }

      /* Collapsible bottom bar. The pane slides fully out of view and hands its
         reserved space back to the content; a small handle stays put to bring
         it back. Transform-only so it stays on the compositor — animating
         `bottom` here would be a layout property, which §5 rules out. */
      [data-bottombar] {
        transition: transform 0.28s cubic-bezier(.16,1,.3,1);
        will-change: transform;
      }
      html.bottombar-collapsed [data-bottombar] {
        transform: translateY(100%);
      }
      /* Collapsed, the only thing still owed space is the handle itself. */
      html.bottombar-collapsed #root {
        padding-bottom: calc(38px + env(safe-area-inset-bottom));
      }

      /* ── Compact chrome (redesign batch A) ─────────────────────────────
         The bar held 204px of an 832px viewport: three stacked rows whose
         paddings and margins were sized for a scrolling page, not a fixed
         bar. Every override needs !important because the bar's own styles
         are inline in the minified bundle — this is the sanctioned lever.
         --bottom-bar-h keeps itself honest: mobile-native.js re-measures
         the bar, so content clearance follows automatically. */
      [data-bottombar] {
        padding-top: 4px !important;
        padding-bottom: calc(4px + env(safe-area-inset-bottom)) !important;
        gap: 0 !important;
      }
      /* The nav pills carried page margins into the bar. */
      [data-bottombar] > div {
        margin-top: 3px !important;
        margin-bottom: 0 !important;
        padding-top: 3px !important;
        padding-bottom: 3px !important;
      }
      /* Row buttons: tighter block padding, but never below a 40px target.
         Both floors need !important: the weekday buttons carry an inline
         `min-width: 0` (the flex-shrink idiom) and the nav pills inherit a
         38px min-height from an earlier rule — without it the compaction
         would shrink exactly the targets it promised to protect. */
      [data-bottombar] button {
        padding-top: 6px !important;
        padding-bottom: 6px !important;
        min-height: 40px !important;
        min-width: 40px !important;
      }
      /* The 40px floor must also FIT: 7 day buttons + 2 week arrows at 40px
         overflowed the 384px strip, which silently shifted the buttons so a
         real finger tap on SUN landed on the next-week arrow (caught by the
         swipe-rollover e2e test, not by eye). The arrows cede width — they
         are secondary to the day targets — and the bar's side padding
         narrows to make the arithmetic work: 8+8 + 32+32 + 7×40 = 360 ≤ 384. */
      [data-bottombar] {
        padding-left: 8px !important;
        padding-right: 8px !important;
      }
      /* 26px, not 32: with the strip's own inter-button gaps the 32px
         arrows still overlapped SUN by 12px (measured, not eyeballed). The
         arrows are the rarest tap in the bar; the seven day targets and the
         gaps win the remaining width. */
      [data-bottombar] button[title="Previous week"],
      [data-bottombar] button[title="Next week"] {
        min-width: 26px !important;
        padding-left: 2px !important;
        padding-right: 2px !important;
      }

      /* ── Desktop finish (redesign batch F) ─────────────────────────────
         Measured at 1440x900: the content column caps at 860px but the
         fixed bar spanned the full 1440 - MON and SUN sat ~1300px apart
         and Train/Body/Insights stretched wall to wall. The bar's frame
         stays full-bleed (it reads as chrome); its CONTROLS align to the
         same column as the content they act on. */
      @media (min-width: 1024px) {
        [data-bottombar] > * {
          max-width: 720px;
          width: 100%;
          margin-left: auto !important;
          margin-right: auto !important;
        }
        /* A "-" placeholder was stretching inside a 250px pill: cap the
           value pills and let the row sit left-aligned like the text above
           it. The selector matches the serialized inline grid. */
        #root [style*="grid-template-columns: 40px 1fr 1fr 1fr"] {
          grid-template-columns: 40px minmax(0,150px) minmax(0,150px) minmax(0,150px) 30px 30px !important;
          justify-content: start !important;
        }
      }
      @media (min-width: 1440px) {
        [data-bottombar] > * { max-width: 860px; }
      }

      /* ── Set row: the rare actions move off the edge (touch only) ──────
         Delete-set and add-note sat in two fixed 30px columns on every row
         of every card — six extra targets per 3-set card for two actions
         used a handful of times a month, one of them DESTRUCTIVE and
         sitting a thumb-width from the circle pressed after every set.

         They are not removed: the first four columns are sized to fill the
         row exactly, so the last two sit just past the right edge and a
         swipe on the row brings them in. The row becomes a real scroll
         container, which means the browser does the gesture — no JS, and
         nothing to lose when React re-renders the row.

         That also settles the conflict with day-swipe-nav.js for free: its
         isExcluded() walks up looking for a horizontal scroller by COMPUTED
         style, so a row swipe can never be read as "change the day".
         overscroll-behavior-x stops the gesture chaining at the ends.

         Touch only. With a mouse there is no mis-tap to prevent and no
         swipe to discover, so desktop keeps both icons in view. */
      /* The "last session" line exercise-native.js paints under the sets.
         It borrows the row's own track sizes at paint time, so the figures
         sit under the columns they belong to; only the colour and rhythm
         live here. Colour comes from the card's own text via opacity, so it
         holds in both themes without a second token. */
      .et-lasttime {
        display: grid;
        gap: 4px;
        padding: 1px 2px 2px;
        font-size: 12px;
        line-height: 1.25;
        font-variant-numeric: tabular-nums;
        text-align: center;
        color: inherit;
        opacity: .5;
      }
      .et-lasttime .et-lt-lbl {
        font-size: 10px;
        letter-spacing: .08em;
        text-transform: uppercase;
      }

      /* The rest clock. Accent-tinted rather than solid so it reads as a
         state the card is in, not a fifth control competing with the set
         rows above it. Both themes get the same treatment because the
         accent works on either ground. */
      .et-rest {
        display: flex;
        align-items: center;
        gap: 10px;
        margin: 6px 2px 2px;
        padding: 6px 12px;
        border-radius: 16px;
        background: color-mix(in srgb, var(--accent, #A07CFF) 14%, transparent);
      }
      .et-rest .et-rest-l {
        font-size: 12px;
        opacity: .65;
        letter-spacing: .04em;
      }
      .et-rest .et-rest-t {
        flex: 1;
        text-align: left;
        border: none;
        background: transparent;
        padding: 2px 0;
        font-family: inherit;
        font-size: 18px;
        font-weight: 700;
        letter-spacing: -.01em;
        font-variant-numeric: tabular-nums;
        color: var(--accent, #A07CFF);
        cursor: pointer;
      }
      /* The empty day's copy shortcut. Outlined rather than filled: "Add
         exercise" stays the primary action for a genuinely new session. */
      .et-copyday {
        display: block;
        width: 100%;
        margin: 10px 0 0;
        padding: 11px 0;
        border: 1px solid color-mix(in srgb, var(--accent, #A07CFF) 45%, transparent);
        border-radius: 18px;
        background: transparent;
        color: var(--accent, #A07CFF);
        font-family: inherit;
        font-size: 14px;
        font-weight: 700;
        cursor: pointer;
      }

      /* The consistency grid, folded
 to its own summary line. max-height
         rather than hiding children, so the header keeps rendering exactly
         as the app draws it. */
      .et-heat-collapsed {
        max-height: 46px !important;
        overflow: hidden !important;
        cursor: pointer;
        position: relative;
      }
      .et-heat-collapsed::after {
        content: "";
        position: absolute; right: 16px; top: 18px;
        width: 8px; height: 8px;
        border-right: 2px solid currentColor;
        border-bottom: 2px solid currentColor;
        transform: rotate(45deg);
        opacity: .45;
      }

      /* Measurements never logged, folded away behind one row. */
      .et-metric-empty { display: none !important; }
      .et-metric-toggle {
        display: block;
        width: 100%;
        margin: 2px 0 10px;
        padding: 11px 0;
        border: 1px dashed color-mix(in srgb, currentColor 22%, transparent);
        border-radius: 18px;
        background: transparent;
        color: inherit;
        opacity: .65;
        font-family: inherit;
        font-size: 13px;
        font-weight: 600;
        cursor: pointer;
      }

      /* The Insights trend badge.
 It sits INSIDE the row the app renders,
         so it takes only the space it needs and inherits the row's colour;
         the sparkline carries its own stroke because a flat lift and a
         climbing one have to differ at a glance, not on reading. */
      .et-trend {
        display: flex;
        align-items: center;
        gap: 7px;
        margin-top: 6px;
        font-variant-numeric: tabular-nums;
      }
      .et-trend .et-trend-l {
        font-size: 12px;
        font-weight: 700;
        color: var(--success, #5DBF3A);
        letter-spacing: -.01em;
      }
      .et-trend.et-trend-flat .et-trend-l { color: #D9A94E; }
      .et-trend svg { display: block; flex-shrink: 0; }

      /* The session summary.
 Accent-outlined rather than accent-filled: it
         is the last thing on a finished day and should feel like a receipt,
         not a call to action. Colours come from the theme tokens the
         pre-paint script keeps in sync, so it holds on both themes. */
      .et-summary {
        margin: 4px 2px 2px;
        padding: 13px 15px;
        border-radius: 20px;
        border: 1px solid var(--accent, #A07CFF);
        background: color-mix(in srgb, var(--accent, #A07CFF) 9%, transparent);
      }
      .et-summary .et-sum-t {
        font-size: 15px; font-weight: 700; letter-spacing: -.01em;
        color: var(--accent, #A07CFF);
      }
      .et-summary .et-sum-s { font-size: 12px; opacity: .7; margin-top: 1px; }
      .et-summary .et-sum-g {
        display: grid; grid-template-columns: repeat(3, 1fr);
        gap: 8px; margin-top: 11px;
      }
      .et-summary .et-sum-v {
        font-size: 19px; font-weight: 700; letter-spacing: -.015em;
        font-variant-numeric: tabular-nums;
      }
      .et-summary .et-sum-k { font-size: 11px; opacity: .55; margin-top: 1px; }
      .et-summary .et-sum-pb {
        margin-top: 10px; padding-top: 9px;
        border-top: 1px solid color-mix(in srgb, currentColor 14%, transparent);
        font-size: 12px; font-weight: 600;
        color: var(--success, #5DBF3A);
      }

      /* ── MMA & Mobility ────────────────────────────────────────────────
         Three ports from the Workout tab, plus one deletion.

         The deletion first: each entry card sits under a category heading
         and then repeats that category as a pill three lines below it, on
         top of the filter chip that already says the same word. The pill is
         hidden rather than removed, because it is the right control the
         moment the list stops being grouped — exercise-native.js re-decides
         on every paint. */
      .et-cat-dupe { display: none !important; }

      /* The filter chips wrapped to two rows (57px) before any content.
         One line that scrolls sideways costs 26px and keeps every category
         reachable. flex: 0 0 auto stops the chips being squeezed instead of
         overflowing, which is what makes the row scroll at all. */
      .et-catfilter,
      .et-reportrange {
        flex-wrap: nowrap !important;
        overflow-x: auto;
        overscroll-behavior-x: contain;
        scrollbar-width: none;
        -webkit-overflow-scrolling: touch;
      }
      .et-catfilter::-webkit-scrollbar,
      .et-reportrange::-webkit-scrollbar { display: none; }
      .et-catfilter > button,
      .et-reportrange > button { flex: 0 0 auto; }

      /* ── The report sheet ──────────────────────────────────────────────
         884px of panel in a 765px window put the Cancel/Generate row's
         bottom edge at 859 on an 832px screen: the button that makes the
         report started every report off-screen. Pinned to the bottom of
         the scrolling panel, so however long the sheet grows, the action
         stays where the thumb already is. */
      .et-reportsheet .et-report-actions {
        position: sticky;
        bottom: 0;
        z-index: 1;
        margin-top: 10px !important;
        padding: 10px 0 2px;
        /* Opaque, and theme-derived: this bar sits over scrolling content,
           so it needs a real ground rather than a tint — mixed against the
           page background so it is light in paper and dark in midnight.
           See the note on .et-gear-toggle for why the literal was wrong. */
        background: color-mix(in srgb, var(--tp, #F5F3F8) 16%, var(--bg, #000000));
        border-top: 1px solid color-mix(in srgb, currentColor 12%, transparent);
      }

      /* The session receipt, in its two-line form: these tabs have no
         volume or PB to report, so it states what was done and stops. */
      .et-summary.et-subsum { padding: 11px 15px; }

      /* The volume trend keeps the size it is authored at (72px) rather
         than filling the row it now sits on. Stretching it across the card
         was tried and reverted: preserveAspectRatio="none" means it scales
         without distorting, but a trend flattened over 328px reads as a
         near-horizontal line and stops showing the shape it exists for. */

      /* ── The card header's top-right pair ──────────────────────────────
         Focus mode and Settings sat 28px apart to the eye: 4px of flex gap
         plus 12px of padding on each button. Padding is now 7px and the gap
         2px, which halves the visible distance to 16px.

         That shrinks the pitch from 44px to 32px, and THAT is why this rule
         exists. The coarse-pointer block above gives every button a 44px
         invisible ::after hit expander; at a 32px pitch two of those overlap
         by 12px, and since Settings comes later in the DOM it paints on top
         — so taps aimed at the right edge of Focus mode would open the gear
         sheet instead. Dropping min-width lets each expander be exactly as
         wide as its own button again, so the two tile instead of overlapping.
         Height is untouched: the targets stay 44px tall. */
      @media (pointer: coarse) {
        #root button[aria-label="Start focus mode"]::after,
        #root button[aria-label="Exercise settings"]::after {
          min-width: 0;
        }
      }

      /* The per-exercise row is already clickable; only the cue was
         missing. An outline and a chevron, in the gap its own flex
         container had reserved for one. */
      .et-chartcue {
        display: inline-flex !important;
        align-items: center;
        gap: 3px;
        padding: 2px 9px;
        border: 1px solid color-mix(in srgb, currentColor 35%, transparent);
        border-radius: 14px;
      }
      .et-chartcue::after { content: '▾'; font-size: 9px; opacity: .8; }

      /* NOTE on entry deletion, so it is not "fixed" again: the ⊗ on each
         MMA/Mobility entry ALREADY takes two taps. The bundle's handler is
         `onClick: () => i ? (l(), V(15)) : (r(!0), V(8))` — first tap arms
         and buzzes, second deletes — with a useEffect that disarms after
         2s, and the armed state fills the button with the danger colour.
         A guard was written here, and reverted: intercepting the first tap
         swallowed the app's own arming step, so deletion stopped working
         entirely. The touch target is already 44px too, via the
         coarse-pointer `#root button::after` expander above. */

      /* Insights → MMA. The badge replaces the absolute-date line rather
         than joining it: "Last: 2026-09-04" spent a whole row saying what
         "last Fri" says inside this one, so the row carries a trend now and
         is no taller than before. */
      .et-abs-date { display: none !important; }
      .et-mtrend .et-trend-x {
        font-size: 12px;
        opacity: .6;
        letter-spacing: -.01em;
      }
      /* Amber means "stalled" on a lift, where an unchanged top weight is
         the thing worth noticing. Drilling the same technique every week is
         not a warning, so a steady technique reads neutral instead. */
      .et-mtrend.et-trend-flat .et-trend-l { color: #8A8A8A; }

      /* ── The gear sheet ────────────────────────────────────────────────
         925px of body in a 690px window, with the blocks changed once a
         quarter sitting above the ones reached for constantly. The setup
         half folds behind one line; nothing is removed. */
      .et-gear-hidden { display: none !important; }
      .et-gear-toggle {
        width: 100%;
        margin: 11px 0 4px;
        padding: 11px 12px;
        min-height: 44px;
        /* Both of these must come from the THEME, not a literal. The first
           version used `var(--card-elevated, #2A2A2A)` with `color: inherit`,
           and --card-elevated is not one of the four tokens the pre-paint
           script publishes (--bg, --tp, --accent, --success) — so the
           fallback always won. In midnight that happened to look right; in
           paper it painted #1A1A1A text on a #2A2A2A ground: black on black.
           A tint of the text colour adapts to both themes on its own, and
           the fallbacks are Colour Schema 1's near-white. */
        background: color-mix(in srgb, var(--tp, #F5F3F8) 8%, transparent);
        border: 1px solid color-mix(in srgb, currentColor 14%, transparent);
        border-radius: 18px;
        color: var(--tp, #F5F3F8);
        font: inherit;
        font-size: 0.875rem;
        font-weight: 600;
        text-align: left;
        cursor: pointer;
      }
      .et-gear-toggle::after { content: ' ▸'; opacity: .5; }
      .et-gear-toggle.et-gear-open::after { content: ' ▾'; }

      /* The strip already styled for this state and never reached it: the
         condition behind it was `… && !1`. Dimmed means the exercise is
         already on that day, planned or logged. */
      /* !important throughout this block: every one of these buttons carries
         the competing declaration inline (opacity: 1, min-height: 30px), so
         a class rule alone never wins. */
      .et-gearsheet button.et-day-has { opacity: .45 !important; }

      /* The row that is the REASON this sheet gets opened mid-set rendered
         at 38x31px — the smallest target in the app, with the destructive
         one sitting between the two movement arrows. 44px, and Remove is
         pushed to its own end of the row. */
      .et-gearsheet button[title="Add set"],
      .et-gearsheet button[title="Remove last set"],
      .et-gearsheet button[title="Move up"],
      .et-gearsheet button[title="Move down"],
      .et-gearsheet button[title="Focus mode"],
      .et-gearsheet button[title="Remove"] {
        min-width: 44px !important;
        min-height: 44px !important;
        justify-content: center;
      }
      .et-gearsheet button[title="Remove"] { order: 9; margin-left: auto; }

      /* Finished, waiting to be dismissed.
 Green rather than accent so the
         state reads at a glance from arm's length — the buzz is a bonus
         signal, not the only one. */
      .et-rest.et-rest-over {
        background: color-mix(in srgb, var(--success, #5DBF3A) 16%, transparent);
      }
      .et-rest.et-rest-over .et-rest-t { color: var(--success, #5DBF3A); }
      .et-rest .et-rest-x,
      .et-rest .et-rest-s {
        border: none;
        background: transparent;
        padding: 4px 2px;
        font-family: inherit;
        font-size: 12px;
        color: inherit;
        opacity: .55;
        cursor: pointer;
      }
      /* Muted reads as struck through, so the state is visible without
         reading the word. */
      .et-rest .et-rest-s[aria-pressed="false"] {
        text-decoration: line-through;
        opacity: .4;
      }

      /* Phone widths only. A 1440px touch screen has room for the icons,
         and at that size the desktop column cap above is the right rule —
         without the width bound this one won every tablet-sized touch
         layout too (caught by the desktop pill-width test). */
      @media (hover: none) and (pointer: coarse) and (max-width: 1023px) {
        #root [style*="grid-template-columns: 40px 1fr 1fr 1fr"] {
          /* 52px = the 40px circle + the three 4px gaps before column 5. */
          grid-template-columns:
            40px repeat(3, calc((100% - 52px) / 3)) 30px 30px !important;
          overflow-x: auto;
          overscroll-behavior-x: contain;
          scroll-snap-type: x mandatory;
          scrollbar-width: none;
          -ms-overflow-style: none;
        }
        #root [style*="grid-template-columns: 40px 1fr 1fr 1fr"]::-webkit-scrollbar {
          width: 0; height: 0;
        }
        /* Two resting positions: values, or actions. */
        #root [style*="grid-template-columns: 40px 1fr 1fr 1fr"] > :first-child {
          scroll-snap-align: start;
        }
        #root [style*="grid-template-columns: 40px 1fr 1fr 1fr"] > :last-child {
          scroll-snap-align: end;
        }
      }

      #bottombar-toggle {
        position: fixed;
        left: 50%;
        bottom: 0;
        z-index: 61;                 /* just above the bar, far below modals */
        width: 56px;
        height: 30px;
        padding: 0;
        border: 1px solid var(--tp, #F0F0F0);
        border-bottom: none;
        border-color: color-mix(in srgb, currentColor 18%, transparent);
        border-radius: 16px 16px 0 0;
        background: var(--bg, #000);
        color: var(--tp, #F0F0F0);
        opacity: 0.75;
        display: flex;
        align-items: center;
        justify-content: center;
        cursor: pointer;
        /* Ride on top of the bar when open, sit on the floor when collapsed.
           translate only — see the note above. */
        transform: translate(-50%, calc(-1 * var(--bottom-bar-h, 132px)));
        transition: transform 0.28s cubic-bezier(.16,1,.3,1), opacity 0.2s ease;
      }
      html.bottombar-collapsed #bottombar-toggle {
        transform: translate(-50%, calc(-1 * env(safe-area-inset-bottom)));
        opacity: 1;
      }
      #bottombar-toggle svg {
        /* chevron points DOWN to hide while the bar is open, UP to reveal it
           once collapsed */
        transform: rotate(180deg);
        transition: transform 0.28s cubic-bezier(.16,1,.3,1);
      }
      html.bottombar-collapsed #bottombar-toggle svg { transform: rotate(0deg); }

      /* Run mode hides the whole bottom bar, which left the collapse handle
         floating alone at the screen edge with nothing to collapse. The run
         overlay's ref stamps this class on <html> while it is mounted. */
      html.et-run-mode #bottombar-toggle { display: none; }

      /* Run mode has to hold a whole superset — two cards plus their set
         controls — inside one screen, so the gap between cards tightens.
         The cards are styled inline by React, so !important is the only way to
         reach them from here without editing style objects in the minified
         bundle. Scoped to et-run-mode, so normal browsing keeps its roomier
         spacing. Padding is deliberately not touched: the card element itself
         has none, it sits on the blocks inside. */
      html.et-run-mode [data-ex-card] { margin-bottom: 6px !important; }

      /* Card entrance wave.
         The brief lists this as an existing invariant; it was not in the repo
         (no animationDelay anywhere, no keyframes for it) so this builds it.
         Transform+opacity only, so it stays on the compositor. The delay is
         set per card by mobile-native.js as --wave-delay and capped there, so a
         long day does not turn into a slow cascade.
         It is triggered ONLY when the selected day changes -- checking off a
         set patches in place and must never replay it, which is the failure
         mode §2 calls out. */
      @keyframes etWaveIn {
        from { opacity: 0; transform: translate3d(0, 10px, 0); }
        to   { opacity: 1; transform: none; }
      }
      /* NOTE: no will-change here, deliberately. will-change: transform makes
         the card a containing block for position:fixed descendants -- and the
         gear popup is rendered INSIDE the card. Because the class used to be
         left on after the animation, every card stayed a containing block for
         good and the popup was clipped to the card's 690x247 box instead of
         filling the viewport. The class is now also removed on animationend;
         dropping will-change is the belt to that braces. */
      [data-ex-card].et-wave-in {
        animation: etWaveIn var(--dur-slow) var(--ease-spring) var(--wave-delay, 0ms) both;
      }

      /* Set-completion pop -- the most repeated interaction in the app, and the
         one moment worth spending motion on. Scale only, so it composites and
         cannot shift the grid around it.
         A CSS animation outranks the inline transform:scale(1) React puts on
         the chip, so no !important is needed; it also overrides the chip's own
         0.15s transform transition for the duration of the pop. */
      @keyframes etSetPop {
        0%   { transform: scale(1); }
        38%  { transform: scale(1.3); }
        70%  { transform: scale(0.94); }
        100% { transform: scale(1); }
      }
      [data-ex-card] button.et-set-pop {
        animation: etSetPop var(--dur-base) var(--ease-spring) both;
      }
      @media (prefers-reduced-motion: reduce) {
        [data-ex-card].et-wave-in { animation: none; opacity: 1; transform: none; }
        [data-ex-card] button.et-set-pop { animation: none; transform: none; }
      }

      /* Native scroll edges: no accidental pull-to-refresh in browser-tab use,
         no scroll chaining bleeding through open sheets. */
      html, body { overscroll-behavior-y: contain; }


      @media (prefers-reduced-motion: reduce) {
        *, *::before, *::after {
          animation-duration: 0.001ms !important;
          animation-iteration-count: 1 !important;
          transition-duration: 0.001ms !important;
        }
      }
