:root {
    /* Brand palette: the --jim-* role tokens below map onto these, so re-theming happens here. */
    --brand-purple: #6859A5;
    --brand-green:  #7C994C;
    --brand-orange: #EE823B;
    --brand-navy:   #0B2735;
    --brand-peach:  #FFEAE0;
    --brand-blue:   #4C7999;

    /* ---- Semantic roles (the designer's hierarchy) ----
       primary = purple, secondary = green, tertiary = orange, accent = blue
       (used in a pinch). Navy is the "black" (text); peach is the "white"
       (backgrounds). Everything below maps onto these, so re-theming is here. */
    --jim-primary:   var(--brand-purple);
    --jim-secondary: var(--brand-green);
    --jim-tertiary:  var(--brand-orange);
    --jim-accent:    var(--brand-blue);

    /* deeper primary for hover + dark-on-light text, mixed from the primary so there's
       no separate hex to keep in sync. */
    --jim-primary-dark: color-mix(in srgb, var(--jim-primary) 82%, black);
    /* pale primary for the unselected half of the range split button. */
    --jim-primary-pale: color-mix(in srgb, var(--jim-primary) 15%, white);

    --jim-ink:        var(--brand-navy);   /* "black": text */
    --jim-bg:         #FFFFFF;             /* white page background + inset tiles/pills */
    --jim-surface:    var(--brand-peach);  /* boxes/cards use the brand peach */
    --jim-muted:      #64748B;
    --jim-border:     #E2E8F0;
    --jim-info:       var(--jim-accent);   /* status blue (e.g. "cloud backup pending" dot) */
    /* The red already used ad-hoc for destructive/failed states (delete, mic error). Tokenised
       for the toast; the older literal #EF4444 spots can move onto this as they're touched. */
    --jim-danger:     #EF4444;

    /* Graph series: weight = secondary (green), reps = tertiary (orange). */
    --jim-graph-weight: var(--jim-secondary);
    --jim-graph-reps:   var(--jim-tertiary);

    /* Graph value bubble: white pill (same as the page), series values keep their colour. */
    --jim-tip-bg:       var(--jim-bg);
    --jim-tip-ink:      var(--brand-navy);

    --jim-nav-h: 64px;

    /* Exercise cards are plain white (no peach); the rep/weight pills inside them get a
       faint grey so they still read as tappable tiles against the card. */
    --jim-card:  #FFFFFF;
    --jim-inset: #F2F2F2;

    /* Page background behind the white cards: the designer's soft lavender grey. Inputs,
       pills and tiles keep --jim-bg (white), so they still read against it. */
    --jim-page-bg: #F4F4F8;

    --jim-nav-bg: var(--brand-purple);
    --jim-nav-radius: 32px;
    /* Width of the centred phone column on desktop; the nav and sheets match it. */
    --jim-app-max-width: 560px;

    /* The bottom system inset everything that clears the nav bar should use. iOS reports its
       home-indicator strip via env(); some Android installs draw the page UNDER the system
       nav buttons (edge-to-edge) yet report a zero inset, so index.html measures that case
       and sets --jim-android-nav-inset. Whichever is larger wins. */
    --jim-safe-bottom: max(env(safe-area-inset-bottom, 0px), var(--jim-android-nav-inset, 0px));

    /* App-wide typeface (loaded in index.html); system fonts if it can't load, e.g. offline. */
    --jim-font: 'Google Sans', system-ui, -apple-system, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;

    /* The house "arrival" motion: overshoots ~10% past the resting place, then eases straight
       back to it with no second wobble. Only for things arriving or growing. Exits and
       collapses keep a plain ease, because an overshoot on the way out would pull a
       collapsing card under its own header. Raise the 1.56 for a bigger overshoot. */
    --jim-ease-elastic: cubic-bezier(0.34, 1.56, 0.64, 1);
    --jim-elastic-duration: 450ms;

    /* Bootstrap bridge */
    --bs-font-sans-serif: var(--jim-font);
    --bs-body-font-family: var(--jim-font);
    --bs-primary: var(--jim-primary);
    --bs-body-bg: var(--jim-page-bg);
    --bs-body-color: var(--jim-ink);
}

:root[data-theme="dark"] {
    --jim-ink:     #E5E7EB;
    --jim-bg:      #0F172A;
    --jim-surface: #1E293B;
    --jim-muted:   #94A3B8;
    --jim-border:  #334155;
    --jim-primary-pale: color-mix(in srgb, var(--jim-primary) 30%, transparent); /* subtle primary tint on the dark surface */
    --jim-page-bg:  #0F172A;
    --jim-nav-bg:   color-mix(in srgb, var(--brand-purple) 70%, #0F172A);
    --jim-card:  #1E293B;
    --jim-inset: #0F172A;
}

*, *::before, *::after { box-sizing: border-box; }

html, body {
    height: 100%;
    margin: 0;
    font-family: var(--jim-font);
    background: var(--jim-page-bg);
    color: var(--jim-ink);
    -webkit-tap-highlight-color: transparent;
    /* Kill horizontal rubber-banding and, where the browser honours it, the swipe
       left/right = back/forward gesture. Belt-and-braces: the real fix for the
       "duplicate home page" on iOS is not stacking duplicate history entries
       (see the replace:true NavigateTo calls). overscroll-behavior does NOT
       reliably suppress Safari's system edge-swipe, so it can't stand alone.
       Vertically, none turns off pull-to-refresh (Dan, 2026-10-18), and with it the iOS
       rubber-band bounce at the top and bottom of the page. */
    overscroll-behavior: none;
}

/* Always reserve the scrollbar's width on desktop, so the centred column doesn't jump
   sideways between a long page (Home, scrollbar showing) and a short one (Timer, Profile,
   no scrollbar). Phones use overlay scrollbars that take no width, so this is a no-op there. */
html { scrollbar-gutter: stable; }

a { color: var(--jim-secondary); text-decoration: none; }

h1, h2, h3, p { margin: 0; }

/* FocusOnNavigate moves focus to the page <h1> on route change (a screen-reader
   nav aid). Hide the visible focus ring on headings — they aren't interactive. */
h1:focus { outline: none; }

.jim-app {
    display: flex;
    flex-direction: column;
    min-height: 100dvh;
    max-width: var(--jim-app-max-width);
    margin: 0 auto;
    background: var(--jim-page-bg);
    position: relative;
    box-shadow: 0 0 40px rgba(0, 0, 0, 0.04);
}

.jim-content {
    flex: 1;
    padding: 0 1rem calc(var(--jim-nav-h) + 1rem + var(--jim-safe-bottom));
}

.jim-page-header {
    padding: calc(1.25rem + env(safe-area-inset-top)) 0 0.75rem;
}
.jim-page-header-center { text-align: center; }
.jim-page-header h1 {
    font-size: 1.6rem;
    font-weight: 800;
    letter-spacing: -0.02em;
}
.jim-page-sub {
    margin-top: 0.2rem;
    color: var(--jim-muted);
    font-size: 0.9rem;
}

.wo-add-row {
    display: flex;
    justify-content: center;
    gap: 0.75rem;
    margin: 1.25rem 0 0.5rem;
}
.jim-add-btn {
    width: 56px;
    height: 56px;
    padding: 0;
    border: none;
    border-radius: 16px;
    cursor: pointer;
    background: var(--jim-primary);
    color: #fff;
    display: flex;
    align-items: center;
    justify-content: center;
    box-shadow: 0 4px 12px color-mix(in srgb, var(--jim-primary) 40%, transparent);
    transition: transform .1s ease, background .15s ease;
}
.jim-add-btn:hover { background: var(--jim-primary-dark); color: #fff; }
.jim-add-btn:active { transform: scale(0.92); }
.jim-add-btn svg { width: 24px; height: 24px; }

.add-sheet-ex { display: flex; align-items: center; gap: 0.6rem; margin-bottom: 1.5rem; }
.add-sheet-icon {
    width: 46px;
    height: 46px;
    border-radius: 12px;
    background: var(--jim-bg);
    display: grid;
    place-items: center;
    font-size: 1.6rem;
    flex: 0 0 auto;
}
.add-sheet-ex .sheet-title { margin: 0; }
.add-stepper { width: 100%; justify-content: space-between; margin-top: 0.2rem; }
.add-reps-input {
    width: 5rem;
    text-align: center;
    border: 1px solid var(--jim-ink);
    border-radius: 9px;
    background: var(--jim-bg);
    color: var(--jim-ink);
    font-weight: 800;
    font-size: 1.2rem;
    padding: 0.3rem 0.4rem;
    -moz-appearance: textfield;
}
.add-reps-input::-webkit-outer-spin-button,
.add-reps-input::-webkit-inner-spin-button { -webkit-appearance: none; margin: 0; }
.add-stepper + .sheet-actions { margin-top: 1.6rem; }

.add-custom-check {
    display: flex;
    align-items: center;
    gap: 0.6rem;
    margin: 1.1rem 0;
    font-weight: 600;
    color: var(--jim-ink);
    cursor: pointer;
}
.add-custom-check input[type="checkbox"] {
    width: 1.2rem;
    height: 1.2rem;
    accent-color: var(--jim-primary);
    flex: 0 0 auto;
}
.add-custom-error { color: #EF4444; font-size: 0.85rem; margin: 0.8rem 0 0; }

/* The .trip-* names are inherited from an earlier app; the markup is Home.razor's
   empty state. Rename together with the markup if it ever becomes worth it. */
.trip-empty {
    text-align: center;
    padding: 3rem 1.5rem;
    color: var(--jim-muted);
}
.trip-empty-icon {
    width: min(240px, 64vw);
    aspect-ratio: 1 / 1;
    margin: 0 auto 1rem;
}
/* The 'Squat Anim' artboard paints its own WHITE square, which would show as a white tile
   on the grey page. Multiply turns white into exactly the page colour underneath and leaves
   the art's colours as they are (only the white goes). Light theme only: in dark mode multiply
   would darken Jymbo too, so there he keeps the white panel, as in the greeting modal. The
   real fix is a transparent backdrop in the .riv. */
:root:not([data-theme="dark"]) .trip-empty-icon .rive-mascot { mix-blend-mode: multiply; }
/* Dark mode: round the white panel off so it reads as a card, not a hard-edged block. */
:root[data-theme="dark"] .trip-empty-icon { border-radius: 1.5rem; overflow: hidden; }
.trip-empty p { max-width: 26rem; margin: 0 auto; }

/* Needs to outrank `.trip-empty p` above (class+element beats a bare class), or the
   margin: 0 there wins and the nudge sits flush against the paragraph. */
.trip-empty .home-restore-nudge { margin-top: 2rem; }
.home-restore-nudge { font-size: 0.9rem; color: var(--jim-muted); }
.home-restore-nudge a { color: var(--jim-secondary); font-weight: 600; }

.plan-label { font-weight: 700; font-size: 0.9rem; margin: 1.1rem 0 0.4rem; }
.plan-input {
    width: 100%;
    padding: 0.75rem 0.9rem;
    font-size: 1rem;
    border: 1px solid var(--jim-border);
    border-radius: 12px;
    background: var(--jim-bg);
    color: var(--jim-ink);
}
.plan-input:focus {
    outline: none;
    border-color: var(--jim-secondary);
    box-shadow: 0 0 0 3px color-mix(in srgb, var(--jim-primary) 20%, transparent);
}
.plan-stepper {
    display: inline-flex;
    align-items: center;
    gap: 1rem;
    /* No outer border — the bordered input and the buttons carry the control on their own. */
    background: var(--jim-bg);
    border-radius: 9px;
    padding: 0.3rem 0;
    width: fit-content;
}
.plan-stepper button {
    width: 2.2rem;
    height: 2.2rem;
    border: none;
    border-radius: 9px;
    background: var(--jim-bg);
    color: var(--jim-ink);
    font-size: 1.3rem;
    line-height: 1;
    cursor: pointer;
}
.plan-stepper button:active { transform: scale(0.94); }
.plan-stepper span { min-width: 2ch; text-align: center; font-weight: 700; font-size: 1.05rem; }

/* Day headers now sit inside the single .wo-list flex column (so a row can be dragged
   across days within one parent). The 0.7rem flex gap already sits above each header, so
   the margin-top here adds to it to keep the ~1.4rem day separation; the first header
   just takes the list's own top margin. */
.wo-day-head {
    font-size: 0.78rem;
    font-weight: 800;
    text-transform: uppercase;
    letter-spacing: 0.06em;
    color: var(--jim-ink);
    text-align: center;
    margin: 0.7rem 0 0;
}
/* Home has no page header, so the list itself clears the notch / status bar. */
.home-list { padding-top: calc(1.25rem + env(safe-area-inset-top)); }
.wo-day-head:first-of-type { margin-top: 0; }
.wo-list {
    display: flex;
    flex-direction: column;
    gap: 0.7rem;
    margin-top: 0.5rem;
}
/* How far the open card reaches past its neighbours on each side. One animated value drives
   both the card's negative margin and the matching extra padding inside it, so the card
   widens around its rows while they stay exactly where they were. Registered so it can
   transition; it inherits so the rows and the carousel can read it. */
@property --wo-open-inset {
    syntax: '<length>';
    inherits: true;
    initial-value: 0px;
}
.wo-item {
    position: relative;
    /* Closed cards are borderless white on the grey page. The border is kept, transparent,
       so the open card's orange frame below can ease in rather than shift the layout. Top
       and sides are already the open frame's 2px, so opening never nudges the rows. */
    border: 2px solid transparent;
    border-bottom-width: 1px;
    border-radius: 16px;
    box-shadow: 0 1px 2px rgba(0, 0, 0, 0.04);
    overflow: hidden;
    --wo-open-shift: var(--wo-open-inset);
    margin-inline: calc(-1 * var(--wo-open-shift));
    transition: border-color 0.2s ease, --wo-open-inset 0.25s ease; /* no border-width: see the open rule below */
}
/* Mid-transition the inset is an arbitrary fraction, and layout rounds the margin and the
   padding to its 1/64px grid separately, so the rows drift by 1/64px. On a scaled screen that
   is enough to flip them between device pixels: visible microstutter. A quarter pixel lies on
   that grid, so both round identically and cancel exactly. */
@supports (width: round(1px, 1px)) {
    .wo-item { --wo-open-shift: round(var(--wo-open-inset), 0.25px); }
}
/* The open exercise: its header stays white, and the card is marked instead by an orange
   frame that is thicker along the bottom — the same raised-button motif as the orange + and
   mic buttons beneath the list. It also grows a little wider than its neighbours, eased
   rather than snapped; the negative inline margin stays inside .jim-content's 1rem gutter.
   :has() rather than a class on .wo-item, so the class attribute stays static and Blazor
   never overwrites the classes reorder.js adds to it mid-gesture. .is-closing is set by
   js/expand.js while the card shrinks shut, so the frame and width ease back together
   with the height instead of waiting for the body to be removed. */
.wo-item:not(.is-closing):has(> .wo-swipe-content > .wo-row.is-open) {
    border-color: var(--jim-tertiary);
    border-width: 2px 2px 6px;
}
/* Only applies while opening; closing falls back to .wo-item's plain ease. Gated on
   no-preference because this selector outranks the reduced-motion .wo-item rule.
   The bounce lives in the height (js/expand.js). Margin sets the position of everything
   inside the card, so overshooting it makes the rows wobble side to side; it glides out on
   an overshoot-free ease-out instead. Border width snaps rather than transitions: browsers
   draw borders at whole device pixels, so an eased width sits a pixel thin through the
   tail and visibly thickens on the last frame. */
@media (prefers-reduced-motion: no-preference) {
    .wo-item:not(.is-closing):has(> .wo-swipe-content > .wo-row.is-open) {
        transition: border-color 0.2s ease,
                    --wo-open-inset var(--jim-elastic-duration) cubic-bezier(0.22, 1, 0.36, 1);
    }
}
/* Half of .jim-content's 1rem gutter: midway between the screen edge and a closed card. The
   drag clone keeps the inset (its rows must match) but not the margin: .wo-drag-clone's
   margin: 0 wins, as it is sized and placed from the real row's rect. */
.wo-item:not(.is-closing):has(> .wo-swipe-content > .wo-row.is-open) {
    --wo-open-inset: 0.5rem;
}
.wo-item:not(.is-closing):has(> .wo-swipe-content > .wo-row.is-open) :is(.wo-sets, .wo-panels) {
    border-top-color: var(--jim-tertiary);
}
/* Swipe-to-delete: the red bin sits behind, revealed as the content slides left. It
   stays hidden until an actual swipe/removal so it can never peek out from behind a
   row whose content is even a hair out of place. */
.wo-swipe-hint {
    position: absolute;
    inset: 0;
    display: flex;
    align-items: center;
    justify-content: flex-end;
    padding-right: 1.1rem;
    background: #EF4444;
    color: #fff;
    border-radius: 14px;
    margin: 2px;
    opacity: 0;
}
.wo-item.is-swiping .wo-swipe-hint,
.wo-item.wo-removing .wo-swipe-hint { opacity: 1; }
.wo-item.wo-removing {
    transition: height 0.28s ease, margin 0.28s ease, opacity 0.28s ease;
}
.wo-swipe-hint svg { width: 20px; height: 20px; }
.wo-will-delete .wo-swipe-hint { background: #DC2626; }
.wo-swipe-content {
    position: relative;
    background: var(--jim-card);
    border-radius: 8px;
    transition: transform 0.2s ease;
    touch-action: pan-y; /* let vertical scroll through; we own horizontal */
}
.wo-swipe-content.is-dragging { transition: none; }

/* One-shot discoverability hint (see swipe.js playDeleteHint): the first time an
   exercise is expanded each day, its first set row peeks left to reveal the delete
   bin then settles back — a nudge that swipe-to-delete exists. Disabled in Settings. */
.wo-set.wo-hint-on .wo-set-swipe-hint { opacity: 1; }
.wo-set-swipe-content.wo-hint-play {
    animation: wo-delete-hint 1.6s ease-in-out 0.35s 1;
}
@keyframes wo-delete-hint {
    0%, 12%   { transform: translateX(0); }
    34%       { transform: translateX(-76px); } /* peek far enough to show the bin */
    52%       { transform: translateX(0); }
    66%       { transform: translateX(-22px); } /* a smaller second bob */
    80%, 100% { transform: translateX(0); }
}
/* Reduced-motion: don't drop the hint (users would never discover the gesture and
   can't be expected to change OS settings) — reduce it. A single, gentle, shorter
   peek with no bob. Turning it off entirely is the Settings toggle. */
@media (prefers-reduced-motion: reduce) {
    .wo-set-swipe-content.wo-hint-play {
        animation: wo-delete-hint-soft 1.6s ease-in-out 0.35s 1;
    }
}
@keyframes wo-delete-hint-soft {
    0%, 18%   { transform: translateX(0); }
    50%       { transform: translateX(-40px); }
    82%, 100% { transform: translateX(0); }
}
/* Extra side padding so the icon clears the card's 16px corner now the grip is gone. The
   header is also the press-and-hold reorder handle (reorder.js), so a long press must not
   select text or pop the iOS callout. */
.wo-row {
    display: flex;
    align-items: center;
    gap: 0.8rem;
    padding: 0.65rem calc(0.95rem + var(--wo-open-shift, 0px));
    cursor: pointer;
    user-select: none;
    -webkit-user-select: none;
    -webkit-touch-callout: none;
    transition: background-color 0.15s ease;
}
.wo-row { position: relative; } /* positions .wo-ghost */

/* iOS haptic on reorder lift (see reorder.js). An invisible layer of selectable text over the
   header: a long-press on it starts an iOS text selection, which plays the system tick, and
   reorder.js lifts the row at that moment. The glyphs AND the selection highlight are
   transparent, and .is-spent makes it unselectable for the rest of the gesture so no handles
   or Copy menu appear. Off iOS it doesn't exist (Android's long-press selection would cancel
   the touch), and the Haptics switch in Settings removes it too. */
.wo-ghost { display: none; }
html.is-ios .wo-ghost {
    display: block;
    position: absolute;
    inset: 0;
    z-index: 1;
    overflow: hidden;
    color: transparent;
    font-size: 40px;
    line-height: 1;
    word-break: break-all;
    user-select: text;
    -webkit-user-select: text;
    -webkit-touch-callout: none;
}
html.is-ios .wo-ghost::selection { background: transparent; color: transparent; }
html.is-ios .wo-ghost.is-spent { user-select: none; -webkit-user-select: none; }
html.haptics-off .wo-ghost,
.wo-drag-clone .wo-ghost { display: none !important; }
/* Phone app, no keyboard navigation: suppress focus rings and the mobile tap-flash on
   the interactive workout controls. Their outline colour falls back to the element's
   text colour (amber on the split button), so a tap was leaving a stray orange ring. */
.wo-row:focus,
.wo-row:focus-visible { outline: none; }
.wo-item,
.wo-row,
.wo-swipe-content,
.wo-set-swipe-content,
.wo-set-reps {
    -webkit-tap-highlight-color: transparent;
}

/* The lifted row's real node stays in the flow as an invisible gap the list closes
   around; only its space matters while it's picked up. */
.wo-item.wo-drag-source { visibility: hidden; }

.wo-drag-clone {
    position: fixed;
    z-index: 1000;
    margin: 0;
    pointer-events: none;
    box-shadow: 0 14px 30px rgba(11, 39, 53, 0.24);
    transform: scale(1.03);
    will-change: top, left, transform;
}
.wo-drag-clone .wo-sets,
.wo-drag-clone .wo-panels { display: none; } /* chip shows the header only, never the body */

html.wo-reordering,
html.wo-reordering * {
    cursor: grabbing !important;
    user-select: none !important;
    -webkit-user-select: none !important;
}

.wo-sets {
    display: flex;
    flex-direction: column;
    border-top: 1px solid var(--jim-border);
    padding: 0.35rem calc(0.95rem + var(--wo-open-shift, 0px)) 0.5rem;
}
.wo-set {
    position: relative;
    overflow: hidden;
}
.wo-set + .wo-set { border-top: 1px dashed var(--jim-border); }
/* Swipe-to-delete on a single set: the red bin sits behind, revealed as it slides
   left. Hidden until an actual swipe/removal so it can't peek from behind the row. */
.wo-set-swipe-hint {
    position: absolute;
    inset: 0;
    display: flex;
    align-items: center;
    justify-content: flex-end;
    padding-right: 0.5rem;
    background: #EF4444;
    color: #fff;
    border-radius: 8px;
    margin: 2px;
    opacity: 0;
}
.wo-set.is-swiping .wo-set-swipe-hint,
.wo-set.wo-removing .wo-set-swipe-hint { opacity: 1; }
.wo-set-swipe-hint svg { width: 16px; height: 16px; }
.wo-set.wo-will-delete .wo-set-swipe-hint { background: #DC2626; }
.wo-set-swipe-content {
    position: relative;
    display: flex;
    align-items: center;
    justify-content: space-between; /* values left, action buttons right */
    gap: 0.7rem;
    padding: 0.35rem 8px 0.35rem 0.2rem;
    border-radius: 8px;
    background: var(--jim-card);
    transition: transform 0.2s ease;
    touch-action: pan-y; /* let vertical scroll through; we own horizontal */
}
.wo-set-swipe-content.is-dragging { transition: none; }
.wo-set-values {
    display: flex;
    align-items: center;
    gap: 0.7rem;
    min-width: 0;
}
.wo-set.wo-removing {
    transition: height 0.28s ease, margin 0.28s ease, opacity 0.28s ease;
}
.wo-set-reps {
    display: inline-flex;
    align-items: baseline;
    gap: 0.3rem;
    border: 1px solid transparent;
    border-radius: 9px;
    background: var(--jim-inset);
    padding: 0.25rem 0.55rem;
    cursor: text;
    color: inherit;
}
.wo-set-reps:hover { border-color: var(--jim-primary); }
/* Editing reuses the same pill so the unit ("kg"/"reps") keeps its place beside the
   value instead of jumping outside a separate input box. The amber outline marks it
   active; the input inside is chromeless. */
.wo-set-reps.is-editing { border-color: var(--jim-primary); background: var(--jim-card); }
.wo-set-num { font-weight: 800; font-size: 1.05rem; color: var(--jim-ink); }
.wo-set-unit { font-size: 0.6rem; font-weight: 700; letter-spacing: 0.05em; color: var(--jim-muted); }
.wo-set-input {
    /* Width is set inline (calc from digit count) for both weight and reps so the unit
       sits snug; this floors it so one/two-character values stay a steady size. */
    min-width: 1.5rem;
    border: none;
    background: transparent;
    color: var(--jim-ink);
    font-weight: 800;
    font-size: 1.05rem;
    padding: 0;
    text-align: left;
    -moz-appearance: textfield;
    appearance: textfield;
}
.wo-set-input:focus { outline: none; }
.wo-set-input::-webkit-outer-spin-button,
.wo-set-input::-webkit-inner-spin-button { -webkit-appearance: none; margin: 0; }
.wo-set-actions {
    flex: 0 0 auto;
    display: flex;
    align-items: center;
    gap: 0.4rem;
}
.wo-set-btn {
    --wo-set-btn-bg: transparent;

    position: relative; /* hosts the HapticSwitch overlay */
    flex: 0 0 auto;
    width: 34px;
    height: 34px;
    border: none;
    border-radius: 9px;
    cursor: pointer;
    display: flex;
    align-items: center;
    justify-content: center;
    background: var(--wo-set-btn-bg);
    color: color-mix(in srgb, var(--wo-set-btn-bg) 35%, white);
    transition: background 0.15s ease;
    -webkit-tap-highlight-color: transparent;
}
.wo-set-btn svg { width: 19px; height: 19px; }
.wo-set-btn:focus,
.wo-set-btn:focus-visible { outline: none; }
.wo-set-btn:active { background: color-mix(in srgb, var(--wo-set-btn-bg) 85%, black); }
.wo-set-del { --wo-set-btn-bg: #E57A72; }
.wo-set-repeat { --wo-set-btn-bg: #F2C335; padding: 2px 0 0 2px; }

/* Two swipeable pages beneath the row header. Structure matters for smoothness: the CLIP
   (.wo-panels-view) is a plain block whose height carousel.js animates to the active page's
   height; the SCROLLER (.wo-panels-scroll) inside it keeps a constant height (the taller
   page) and owns the horizontal snap-scroll. Keeping the resized element separate from the
   scrolling element is what stops iOS from hitching when the box grows/shrinks mid-swipe. */
/* The open-card inset goes around the carousel, not inside its pages: if the scroller grew
   with the card, its scrollLeft (set once, for the start page) would fall behind the widening
   pages and the browser's re-snapping would jitter them sideways. */
.wo-panels { border-top: 1px solid var(--jim-border); padding-inline: var(--wo-open-shift, 0px); }
/* When the carousel is present, the inner set list drops its own divider (the panel
   container owns it) and its bottom padding (the dots supply the breathing room). */
.wo-panels .wo-sets { border-top: none; padding-bottom: 0.15rem; padding-inline: 0.95rem; }
.wo-panels-view {
    overflow: hidden;                 /* clips the taller page to the active height */
    transition: height 0.3s ease;     /* used for content-change refit; a swipe sets height inline */
}
.wo-panels-scroll {
    overflow-x: auto;
    overflow-y: hidden;
    scroll-snap-type: x mandatory;
    overscroll-behavior-x: contain;
    scrollbar-width: none;
    /* No -webkit-overflow-scrolling: touch — it fights CSS scroll-snap on modern iOS. */
}
.wo-panels-scroll::-webkit-scrollbar { display: none; }
.wo-panels-track {
    display: flex;
    align-items: flex-start; /* each page keeps its own height (don't stretch to tallest) */
}
/* The set rows carry touch-action: pan-y (from the disabled swipe-to-delete), which tells
   the browser "vertical panning only" and so swallows the horizontal swipe before it can
   reach the carousel — you could swipe graph→sets (the graph page has no such element) but
   not sets→graph. Inside the carousel, let set rows pan both ways so the swipe gets through
   while vertical list scrolling still works. */
.wo-panels .wo-set-swipe-content { touch-action: pan-x pan-y; }
.wo-panel {
    flex: 0 0 100%;
    min-width: 0;
    scroll-snap-align: start;
    scroll-snap-stop: always;
}

/* Page dots: brand-orange rings, the active page filled in. The ring is an inset shadow,
   not a border, so a dot stays exactly 7px whether it's filled or not. */
.wo-dots {
    display: flex;
    justify-content: center;
    gap: 7px;
    padding: 0.5rem 0 0.55rem;
}
.wo-dot {
    width: 7px;
    height: 7px;
    padding: 0;
    border: none;
    border-radius: 50%;
    background: transparent;
    box-shadow: inset 0 0 0 1.5px var(--jim-tertiary);
    cursor: pointer;
    transition: background 0.2s ease, transform 0.2s ease;
    -webkit-tap-highlight-color: transparent;
}
.wo-dot:focus, .wo-dot:focus-visible { outline: none; }
.wo-dot.is-active { background: var(--jim-tertiary); transform: scale(1.15); }
@media (prefers-reduced-motion: no-preference) {
    .wo-dot.is-active { transition: background 0.2s ease, transform var(--jim-elastic-duration) var(--jim-ease-elastic); }
}

@media (prefers-reduced-motion: reduce) {
    .wo-item, .wo-panels-view, .wo-dot { transition: none; }
}
.wo-icon {
    flex: 0 0 auto;
    width: 52px;
    height: 52px;
    border-radius: 12px;
    background: var(--jim-bg);
    display: grid;
    place-items: center;
    font-size: 1.7rem;
    overflow: hidden;
}
.wo-name {
    flex: 1;
    min-width: 0;
    font-weight: 700;
    font-size: 0.98rem;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}
.wo-stat {
    flex: 0 0 auto;
    display: flex;
    flex-direction: column;
    align-items: center;
    min-width: 2.6rem;
}
.wo-num { font-weight: 800; font-size: 1.2rem; line-height: 1; color: var(--jim-ink); }
.wo-lbl {
    margin-top: 2px;
    font-size: 0.6rem;
    font-weight: 700;
    letter-spacing: 0.05em;
    color: var(--jim-muted);
}
.wo-del {
    flex: 0 0 auto;
    width: 32px;
    height: 32px;
    border: none;
    border-radius: 9px;
    background: transparent;
    color: var(--jim-muted);
    cursor: pointer;
    display: flex;
    align-items: center;
    justify-content: center;
}
.wo-del:hover { background: rgba(239, 68, 68, 0.12); color: #EF4444; }
.wo-del svg { width: 16px; height: 16px; }

/* The markup mic is AddExerciseButtons' fallback for when Rive can't start. Shape and colour
   come from .jim-add-btn; this adds only the hold behaviour + states. */
.jim-mic-btn {
    /* Push-and-hold: don't scroll, select text, or pop the long-press callout. */
    touch-action: none;
    user-select: none;
    -webkit-user-select: none;
    -webkit-touch-callout: none;
}
.jim-mic-btn.starting {
    background: var(--jim-primary-dark);
    animation: mic-warm 1s ease-in-out infinite;
}
@keyframes mic-warm {
    0%, 100% { opacity: 0.75; }
    50%      { opacity: 1; }
}
.jim-mic-btn.listening {
    background: #EF4444;
    animation: mic-pulse 1.2s ease-in-out infinite;
}
@keyframes mic-pulse {
    0%   { box-shadow: 0 0 0 0 rgba(239, 68, 68, 0.45); }
    70%  { box-shadow: 0 0 0 14px rgba(239, 68, 68, 0); }
    100% { box-shadow: 0 0 0 0 rgba(239, 68, 68, 0); }
}
.jim-mic-btn.processing { background: var(--jim-primary-dark); }
.jim-mic-spinner {
    width: 24px;
    height: 24px;
    border: 3px solid rgba(255, 255, 255, 0.4);
    border-top-color: #fff;
    border-radius: 50%;
    animation: spin 0.7s linear infinite;
}
/* iOS has no separate system nav bar — the home-indicator strip is part of the page's
   own safe area. The bar fills that strip with its own background (PADDING, not margin):
   a margin left a see-through gap under the bar that the list visibly scrolled through.
   Android normally reports a zero inset; when it draws under its own nav buttons instead,
   --jim-safe-bottom picks up the measured fallback so the buttons sit in the padding, not
   on the icons. Everything else clears the bar with `var(--jim-nav-h) + var(--jim-safe-bottom)`
   so it all lines up. Rounded top corners, no border — the colour does the separating. */
.jim-nav {
    position: fixed;
    bottom: 0;
    left: 50%;
    transform: translateX(-50%);
    width: 100%;
    max-width: var(--jim-app-max-width);
    display: flex;
    background: var(--jim-nav-bg);
    border-radius: var(--jim-nav-radius) var(--jim-nav-radius) 0 0;
    padding-bottom: var(--jim-safe-bottom);
    /* Side padding (1/10 of the bar width) pulls the outer icons in off the edges. 1/8 would
       make edge-to-icon equal icon-to-icon exactly; 1/10 looked more balanced. */
    padding-inline: calc(min(100vw, 560px) / 10);
    z-index: 100;
}
.jim-nav-item {
    flex: 1;
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 2px;
    padding: 1.25rem 0;
    color: var(--jim-muted);
    font-size: 0.72rem;
    font-weight: 600;
}
.jim-nav-item:hover { color: var(--jim-ink); }
.jim-nav-item.active { color: var(--jim-primary); }
.jim-nav-icon { position: relative; display: flex; align-items: center; justify-content: center; height: 24px; }
.jim-nav-icon svg { width: 24px; height: 24px; }
/* The designer's solid icons (Workout / Profile) keep their own proportions at a fixed
   height. Inactive uses the designer's slate; active uses the brand highlight above. The
   item's padding drops by 2px a side for the 4px-taller icon, so the bar stays --jim-nav-h. */
.jim-nav-item:has(.jim-nav-icon-workout, .jim-nav-icon-profile, .jim-nav-icon-timer) { padding: 1.125rem 0; color: #546770; }
.jim-nav-item.active:has(.jim-nav-icon-workout, .jim-nav-icon-profile, .jim-nav-icon-timer) { color: var(--jim-primary); }
.jim-nav-icon-workout, .jim-nav-icon-profile, .jim-nav-icon-timer { height: 28px; }
.jim-nav-icon-workout { padding-top: 4px; }
.jim-nav-icon-workout svg, .jim-nav-icon-profile svg, .jim-nav-icon-timer svg { width: auto; height: 28px; }
:root[data-theme="dark"] .jim-nav-item:has(.jim-nav-icon-workout, .jim-nav-icon-profile, .jim-nav-icon-timer):not(.active) { color: var(--jim-muted); }
.jim-nav-label {
    max-width: 6.5rem;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

#blazor-error-ui {
    display: none;
    align-items: center;
    gap: 0.6rem;
    position: fixed;
    left: 12px;
    right: 12px;
    bottom: calc(var(--jim-nav-h, 60px) + var(--jim-safe-bottom, 0px) + 12px);
    background: #FDE4E1;
    color: #8A1C1C;
    border: 1px solid #F3B0AB;
    padding: 0.8rem 0.85rem 0.8rem 1.1rem;
    border-radius: 14px;
    box-shadow: 0 10px 30px rgba(127, 29, 29, 0.22);
    z-index: 3000;
    transform: translateY(calc(100% + 40px));
    opacity: 0;
    transition: transform 0.34s cubic-bezier(0.2, 0.8, 0.2, 1), opacity 0.26s ease;
}
#blazor-error-ui.jim-error-show { transform: translateY(0); opacity: 1; }
#blazor-error-ui .jim-error-msg { flex: 1 1 auto; font-weight: 600; font-size: 0.92rem; line-height: 1.3; }
#blazor-error-ui .reload {
    flex: 0 0 auto;
    font-weight: 700;
    font-size: 0.88rem;
    text-decoration: none;
    color: #fff;
    background: #DC2626;
    padding: 0.42rem 0.8rem;
    border-radius: 9px;
}
#blazor-error-ui .reload:active { background: #B91C1C; }
#blazor-error-ui .dismiss {
    flex: 0 0 auto;
    display: grid;
    place-items: center;
    width: 30px;
    height: 30px;
    padding: 0;
    border: none;
    background: transparent;
    color: #8A1C1C;
    font-size: 0.95rem;
    line-height: 1;
    cursor: pointer;
    border-radius: 8px;
    -webkit-tap-highlight-color: transparent;
}
#blazor-error-ui .dismiss:active { background: rgba(138, 28, 28, 0.12); }
:root[data-theme="dark"] #blazor-error-ui {
    background: #3B1A1A;
    color: #FCA5A5;
    border-color: #7F1D1D;
}
:root[data-theme="dark"] #blazor-error-ui .dismiss { color: #FCA5A5; }
@media (prefers-reduced-motion: reduce) {
    #blazor-error-ui { transition: opacity 0.2s ease; transform: none; }
}

.sheet-backdrop {
    position: fixed;
    inset: 0;
    z-index: 200;
    background: rgba(12, 26, 34, 0.45);
    display: flex;
    align-items: flex-end;
    justify-content: center;
    animation: sheet-fade 0.2s ease;
}
.sheet {
    width: 100%;
    max-width: var(--jim-app-max-width);
    /* Card white rather than the peach surface — the sheet reads as a panel lifted off the
       page, same as the exercise cards, and it still darkens correctly in dark mode. */
    background: var(--jim-card);
    border-radius: 20px 20px 0 0;
    padding: 0.6rem 1.5rem calc(1.5rem + var(--jim-safe-bottom));
    box-shadow: 0 -6px 24px rgba(0, 0, 0, 0.18);
    animation: sheet-up 0.32s cubic-bezier(0.32, 0.72, 0, 1);
}
.sheet-handle {
    width: 40px;
    height: 4px;
    border-radius: 2px;
    background: var(--jim-ink);
    margin: 0.25rem auto 1rem;
}
.sheet-title { font-size: 1.15rem; font-weight: 700; color: var(--jim-ink); margin: 0 0 0.4rem; }
.sheet-msg { color: var(--jim-muted); font-size: 0.9rem; line-height: 1.45; margin: 0 0 1.25rem; }
.sheet-actions { display: flex; gap: 0.75rem; }
.sheet-btn {
    flex: 1;
    padding: 0.8rem 1rem;
    border-radius: 12px;
    border: none;
    font-weight: 700;
    font-size: 0.95rem;
    cursor: pointer;
}
.sheet-cancel { background: var(--jim-bg); color: var(--jim-ink); border: 1px solid var(--jim-border); }
.sheet-confirm { background: var(--jim-primary); color: #fff; }
.sheet-danger { background: #EF4444; color: #fff; }
@keyframes sheet-up { from { transform: translateY(100%); } to { transform: translateY(0); } }
@keyframes sheet-fade { from { opacity: 0; } to { opacity: 1; } }

@keyframes sheet-down { from { transform: translateY(0); } to { transform: translateY(100%); } }
@keyframes sheet-fade-out { from { opacity: 1; } to { opacity: 0; } }
.sheet.closing { animation: sheet-down 0.3s cubic-bezier(0.32, 0.72, 0, 1) forwards; }
.sheet-backdrop.closing { animation: sheet-fade-out 0.25s ease forwards; }

/* A haptic host must be a positioning context so its switch overlay can cover it.
   Harmless off iOS, where the switch is display:none. */
.has-haptic { position: relative; }

/* The native iOS switch, laid invisibly over its host. Hidden on every non-iOS
   browser — Android's tick comes from navigator.vibrate, not from this element. */
.haptic-switch { display: none; }

html.is-ios .haptic-switch {
    display: block;
    position: absolute;
    inset: 0;
    width: 100%;
    height: 100%;
    margin: 0;
    padding: 0;
    opacity: 0;
    /* Clip hit-testing to the host's box. border-radius on the host clips paint but not
       taps; clip-path clips both, so taps past a rounded corner don't tick. */
    clip-path: inset(0);
    /* Above the host's own content so the finger toggles the switch (firing the tick),
       not the icon/label. The click still bubbles to the host, so its handler runs. */
    z-index: 2;
    cursor: pointer;
    /* Do NOT reset -webkit-appearance here: the native switch rendering is what carries
       the Taptic tick. Stripping it silences the haptic. */
}
html.is-ios .haptic-switch--round { clip-path: inset(0 round 999px); }
html.is-ios .jim-add-btn .haptic-switch { clip-path: inset(0 round 16px); }

/* Settings toggle off: taps fall through to the host, so no switch toggles and no tick
   fires. (The Android navigator.vibrate path is gated separately in haptics.js.) */
html.haptics-off .haptic-switch { pointer-events: none; }

/* The artboard carries its own padding around the two buttons, so the row sits tighter
   than the markup fallback. */
.wo-add-row-rive { margin: 0.25rem 0 0; }

.jim-rive-add {
    position: relative;
    display: block;
    max-width: 100%;
}
/* Drawn at 1 artboard unit = 1 CSS px (211 × 119); the bridge re-applies both from the
   file's real bounds on load, so a re-authored artboard needs no edit here. Long presses
   must not select, scroll or pop the iOS callout mid-recording. */
.jim-rive-add-canvas {
    display: block;
    width: 211px;
    max-width: 100%;
    aspect-ratio: 211 / 119;
    height: auto;
    touch-action: none;
    -webkit-touch-callout: none;
    -webkit-user-select: none;
    user-select: none;
}

/* iOS haptic overlays. Off iOS they never render, so Rive hit-tests the canvas itself. */
.jim-rive-add-hap { display: none; }
html.is-ios .jim-rive-add-hap {
    display: block;
    position: absolute;
    inset: 0;
    z-index: 3;
    pointer-events: none; /* only the two regions below are live */
}
html.is-ios .jim-rive-add-hap > .has-haptic {
    position: absolute;
    top: 20%;
    height: 60%;
    pointer-events: auto;
    touch-action: none;
    -webkit-touch-callout: none;
    -webkit-user-select: none;
    user-select: none;
}
html.is-ios .jim-rive-add-hap .haptic-switch { clip-path: inset(0 round 16px); }
/* Measured from the .riv's listener hit areas (fractions of the artboard). If the designer
   moves the buttons, re-measure and update these four numbers. */
html.is-ios .jim-rive-add-hap > [data-rive-btn="add"] { left: 15%; width: 35%; }
html.is-ios .jim-rive-add-hap > [data-rive-btn="mic"] { left: 50%; width: 35%; }

/* The three nav buttons are drawn and hit-tested inside this canvas, so it fills the
   bar and nothing may be layered over it without pointer-events: none. touch-action /
   callout / user-select are all off because a long press on iOS otherwise pops the
   text-selection callout in the middle of a voice recording. */
/* Sized to the artboard's own aspect rather than stretched across the bar. When the
   canvas was full-width, the letterboxed strips either side of the art were still
   part of the canvas box, so they swallowed every tap that landed on them — dead
   pixels that also blocked the content scrolling underneath. Matching the canvas to
   the art means there are no such strips. rive-panel.js overwrites both values from
   the artboard's real bounds on load, so a re-authored artboard adapts by itself. */
.jim-rive-panel {
    display: block;
    aspect-ratio: 500 / 119;
    width: calc(var(--jim-nav-h) * 500 / 119);
    max-width: 100%;
    height: auto;
    pointer-events: auto;
    touch-action: none;
    -webkit-touch-callout: none;
    -webkit-user-select: none;
    user-select: none;
}

/* Shrink-wraps the canvas so the overlays inside can use inset:0 and land exactly on
   the panel rather than on the (wider) bar. */
.jim-rive-wrap {
    position: relative;
    display: block;
    max-width: 100%;
}

/* Off iOS this never renders, so Rive keeps hit-testing the canvas itself. */
.jim-rive-hap { display: none; }

html.is-ios .jim-rive-hap {
    display: block;
    position: absolute;
    inset: 0;
    /* Above the canvas: the tap has to reach the native switch, not the artwork. */
    z-index: 3;
    /* Only the three regions below are live, so nothing else is stolen from the page. */
    pointer-events: none;
}

html.is-ios .jim-rive-hap > .has-haptic {
    position: absolute;
    top: 0;
    bottom: 0;
    pointer-events: auto;
    /* Same reasons as the canvas: no scroll-hijack, and no iOS text-selection callout
       popping up in the middle of a press-and-hold recording. */
    touch-action: none;
    -webkit-touch-callout: none;
    -webkit-user-select: none;
    user-select: none;
}

/* Thirds of the artboard. Assumption, not something the .riv declares — kept here in
   one place so re-tuning to a new layout is three numbers. */
html.is-ios .jim-rive-hap > [data-rive-btn="home"] { left: 0; width: 34%; }
html.is-ios .jim-rive-hap > [data-rive-btn="mic"] { left: 34%; width: 32%; }
html.is-ios .jim-rive-hap > [data-rive-btn="profile"] { left: 66%; width: 34%; }

/* Rive paints pixels, not semantics. These controls carry the accessible names and
   keyboard activation for the canvas; they sit underneath it, so pointers always reach
   the graphic and only focus/AT reaches these. */
.jim-rive-a11y {
    position: absolute;
    inset: 0;
    display: flex;
    z-index: -1;
    /* Keyboard focus and screen-reader activation still work with pointer events
       off; this only stops the layer from covering the now-transparent bar and
       intercepting taps meant for the content beneath it. */
    pointer-events: none;
}
.jim-rive-a11y button {
    flex: 1;
    background: none;
    border: 0;
    color: transparent;
    font-size: 0.72rem;
}
.jim-rive-a11y button:focus-visible {
    z-index: 1;
    color: var(--jim-ink);
    outline: 2px solid var(--jim-primary);
    outline-offset: -3px;
}
