/* ==========================================================================
   Run-It shared components (UI consistency pass 2026-09-27).
   Buttons, page title, section card, tile, status pill, select group, form
   fields and the notice line. Values come from the Standard v1 tokens in
   static/ui-foundation.css; the Dashboard is the reference look, and the
   buttons copy run-it.io and the app header (measured, see FOUNDATION.md).

   Loading: render-blocking on every app page, right after ui-shell.css and
   before any page stylesheet. It styles nothing until markup opts in with an
   .ri-* class, so loading it changes no page by itself.

   Cascade:
   - Every rule below is unlayered and class-based (0,1,0 for a component and
     its modifiers, 0,2,0 for states). That beats the Tailwind build (all of it
     is layered) and every element-level global (h1-h6, p, button, a) by
     specificity, without !important. Page stylesheets load later: a page rule
     with more specificity still wins, which is why migrating means swapping
     classes AND deleting the page rules that styled the old markup.
   - Adapt a component through its custom properties (--ri-* tokens, or the
     --_btn-* / --_sel-* properties documented per component), never by
     overriding its properties from page CSS.
   - The @layer ri-lock block at the end holds the few properties that legacy
     global !important rules force onto the elements these components use
     (the copied clamp() type block in seven page stylesheets, the
     .app-page-* phone rules in dashboard-embed.css, the page-level global
     field :focus rules). A layered !important outranks every unlayered
     !important, so no specificity race is needed; once those globals are
     deleted the lock is simply redundant. The h2/h3 cyan, the section h2
     glow and pro-app.css's global field focus were deleted on 2026-09-27
     (F2), and their part of the lock with them.
   ========================================================================== */

@import url('/static/relay-loader.css?v=20261012-saving-spinner-white');

@layer theme, base, components, utilities;

/* --------------------------------------------------------------------------
   Buttons: .ri-btn + one look + optional size / shape.
   Looks:  --primary (run-it.io hero) --cta (pricing card, arrow) --secondary
           (header Sign out) --neutral (Sign out in grey: Cancel)
           --select (My Shoes race pill) --ghost --danger
           --link ("View ..." links, one arrow motion) --lime (Contributor)
   Sizes:  --sm 32 px, md 40 px (default), --lg 48 px. Shape: --icon (square).
   States: :hover, :focus-visible, :disabled / [aria-disabled="true"],
           [aria-busy="true"]; select: [aria-pressed|aria-selected|aria-checked
           ="true"] or .is-selected.
   Pseudo-elements: ::before is the 1 px gradient ring; ::after is the hover
   wash on fine pointers, the 44 px hit area on coarse pointers (the .ri-hit
   approach; do not add .ri-hit to an .ri-btn) and the spinner while busy.
   -------------------------------------------------------------------------- */
.ri-btn {
  --_btn-h: var(--ri-control-md, 40px);
  --_btn-px: 18px;
  --_btn-fs: var(--ri-text-button, 14px);
  --_btn-lh: 20px;
  --_btn-fw: var(--ri-weight-medium, 500);
  --_btn-icon: var(--ri-icon-sm, 16px);
  --_btn-radius: var(--ri-radius-sm, 8px);
  --_btn-color: #fff;
  --_btn-bg: transparent;
  --_btn-bw: 0px;
  --_btn-border: transparent;
  --_btn-shadow: none;
  --_btn-ring: none;
  --_btn-ring-opacity: 1;
  --_btn-wash: none;
  position: relative;
  isolation: isolate;
  box-sizing: border-box;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 6px;
  height: var(--_btn-h);
  margin: 0;
  border: var(--_btn-bw) solid var(--_btn-border);
  border-radius: var(--_btn-radius);
  background: var(--_btn-bg);
  background-origin: border-box;
  box-shadow: var(--_btn-shadow);
  color: var(--_btn-color);
  font-family: var(--ri-font-sans, 'Noto Sans', ui-sans-serif, system-ui, sans-serif);
  font-weight: var(--_btn-fw);
  font-style: normal;
  font-variant-numeric: var(--ri-numeric, tabular-nums);
  letter-spacing: 0;
  text-align: center;
  text-decoration: none;
  text-shadow: none;
  text-transform: none;
  white-space: nowrap;
  vertical-align: middle;
  cursor: pointer;
  user-select: none;
  -webkit-user-select: none;
  touch-action: manipulation;
  -webkit-tap-highlight-color: transparent;
  appearance: none;
  -webkit-appearance: none;
  transform: none;
  transition-property: opacity, box-shadow, border-color, background-color, color;
  transition-duration: var(--ri-dur-fast, 150ms);
  transition-timing-function: var(--ri-ease-standard, cubic-bezier(0.4, 0, 0.2, 1));
}
/* States restate what page-level button:hover / :active rules change. */
.ri-btn:hover,
.ri-btn:active,
.ri-btn:focus {
  border-color: var(--_btn-border);
  box-shadow: var(--_btn-shadow);
  color: var(--_btn-color);
  text-decoration: none;
  transform: none;
}
.ri-btn::before,
.ri-btn::after {
  content: "";
  position: absolute;
  inset: 0;
  border-radius: inherit;
  pointer-events: none;
  transition: opacity var(--ri-dur-fast, 150ms) var(--ri-ease-standard, cubic-bezier(0.4, 0, 0.2, 1));
}
/* The ring: a masked 1 px gradient line inside the edge (run-it.io's
   .pricing-button-gradient / .button-border-gradient construction). */
.ri-btn::before {
  z-index: -1;
  padding: 1px;
  background: var(--_btn-ring);
  opacity: var(--_btn-ring-opacity);
  -webkit-mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
  -webkit-mask-composite: xor;
  mask: linear-gradient(#000 0 0) content-box exclude, linear-gradient(#000 0 0);
}
/* The hover wash (secondary, ghost): a layer that fades, since gradients
   cannot transition. */
.ri-btn::after {
  z-index: -2;
  background: var(--_btn-wash);
  opacity: 0;
}
@media (hover: hover) {
  .ri-btn:hover::after { opacity: 1; }
}
/* 44 x 44 tap target on touch screens; the button keeps its visual size. */
@media (pointer: coarse) {
  .ri-btn::after {
    inset: min(0px, calc(50% - var(--ri-hit-min, 44px) / 2));
    background: none;
    opacity: 1;
    pointer-events: inherit;
  }
}
.ri-btn > svg,
.ri-btn-icon {
  flex: none;
  width: var(--_btn-icon);
  height: var(--_btn-icon);
}
.ri-btn-icon {
  position: relative;
  display: inline-grid;
  place-items: center;
}
.ri-btn-icon > svg {
  width: 100%;
  height: 100%;
}
.ri-btn > .ri-btn-arrow {
  width: 17px;
  height: 16px;
}
.ri-btn:focus-visible {
  outline: 2px solid var(--ri-cyan, #2ED1F4);
  outline-offset: 2px;
}
.ri-btn:focus:not(:focus-visible) { outline: none; }
/* Keyboard focus only: after a tap a popup's close X still matched
   :focus-visible (pro-app.css has the page-wide rule and the why). */
html[data-ri-pointer] .ri-btn:focus-visible { outline: none; }
.ri-btn:disabled,
.ri-btn[aria-disabled="true"] {
  opacity: 0.45;
  cursor: not-allowed;
  pointer-events: none;
}

/* Busy: add aria-busy="true" (and disabled on a <button>). The spinner takes
   the leading .ri-btn-icon's place; without one it sits in the middle and the
   label turns transparent, so the width never changes. relay-loader.css paints
   the shared mark; its SVG stays assembled under reduced motion. */
.ri-btn[aria-busy="true"] {
  opacity: 1;
  cursor: progress;
  pointer-events: none;
}
.ri-btn[aria-busy="true"] .ri-btn-icon > * { visibility: hidden; }
.ri-btn[aria-busy="true"] .ri-btn-icon::before,
.ri-btn[aria-busy="true"]:not(:has(.ri-btn-icon))::after {
  content: "";
  box-sizing: border-box;
  border: 2px solid var(--_btn-color);
  border-right-color: transparent;
  border-radius: 50%;
  background: none;
  opacity: 1;
  animation: ri-btn-spin var(--ri-dur-spin, 800ms) linear infinite;
}
.ri-btn[aria-busy="true"] .ri-btn-icon::before {
  position: absolute;
  inset: 0;
}
.ri-btn[aria-busy="true"]:not(:has(.ri-btn-icon)) { color: transparent; }
.ri-btn[aria-busy="true"]:not(:has(.ri-btn-icon)) > svg { visibility: hidden; }
.ri-btn[aria-busy="true"]:not(:has(.ri-btn-icon))::after {
  inset: calc(50% - 8px) auto auto calc(50% - 8px);
  width: 16px;
  height: 16px;
  z-index: 1;
}
@keyframes ri-btn-spin { to { transform: rotate(360deg); } }

/* Sizes */
.ri-btn--sm {
  --_btn-h: var(--ri-control-sm, 32px);
  --_btn-px: 12px;
  --_btn-fs: var(--ri-text-caption, 13px);
  --_btn-lh: 16px;
}
.ri-btn--lg {
  --_btn-h: var(--ri-control-lg, 48px);
  --_btn-px: 28px;
  --_btn-fs: var(--ri-text-lead, 16px);
  --_btn-lh: 24px;
  --_btn-icon: var(--ri-icon-md, 20px);
}
.ri-btn--sm > .ri-btn-arrow {
  width: 15px;
  height: 14px;
}
.ri-btn--block {
  display: flex;
  width: 100%;
}
.ri-btn--icon {
  --_btn-px: 0px;
  width: var(--_btn-h);
}

/* Primary: the run-it.io hero "Get Your Free Analysis" (48 px, 12 x 28,
   radius 8, 16/500 white, #79F1FF -> #00C2FF, inset shadow, hover 80 %). */
.ri-btn--primary {
  --_btn-bg: var(--ri-gradient-primary, linear-gradient(180deg, #79F1FF -11.11%, #00C2FF 100%));
  --_btn-shadow: inset 0 -3px 10px rgba(0, 0, 0, 0.25);
  --_btn-fw: var(--ri-weight-medium, 500);
}
@media (hover: hover) {
  .ri-btn--primary:hover { opacity: 0.8; }
}
/* Owner rule (2026-10-20): nothing cyan inside a solid cyan button — every icon in it draws
   fully white (two-colour sprites, inline SVGs and icon images alike). */
.ri-btn--primary svg,
.ri-btn--primary .run-it-icon {
  --icon-muted: #ffffff;
  --icon-accent: #ffffff;
  color: #ffffff;
}
.ri-btn--primary img:not([data-keep-color]) { filter: brightness(0) invert(1); }

/* Garmin: Garmin Connect actions (owner 2026-10-07). The primary in black:
   same size, radius, weight, white label and hover; no border. */
.ri-btn--garmin {
  --_btn-bg: #000;
  --_btn-fw: var(--ri-weight-medium, 500);
}
@media (hover: hover) {
  .ri-btn--garmin:hover { opacity: 0.8; }
}

/* CTA: the run-it.io pricing "Get my free analysis ->" (glass wash, cyan
   border, brand-gradient ring inside it, arrow). Hover lights the inner glow. */
.ri-btn--cta {
  --_btn-bg: var(--ri-gradient-glass, linear-gradient(180deg, rgba(255, 255, 255, 0.1) 0%, rgba(255, 255, 255, 0) 100%));
  --_btn-bw: 1px;
  --_btn-border: rgba(46, 209, 244, 0.34);
  --_btn-ring: var(--ri-gradient-ring, linear-gradient(90deg, #79F1FF 0.01%, #38E6D7 50.01%, #00C2FF 100%));
  --_btn-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.1);
  --_btn-fw: var(--ri-weight-medium, 500);
}
@media (hover: hover) {
  .ri-btn--cta:hover {
    --_btn-border: rgba(46, 209, 244, 0.72);
    --_btn-shadow: inset 0 -6px 15px rgba(56, 230, 215, 0.25), inset 0 6px 15px rgba(121, 241, 255, 0.25), 0 0 30px rgba(46, 209, 244, 0.22), inset 0 1px 0 rgba(255, 255, 255, 0.18);
  }
}

/* Secondary: the app header "Sign out" (glass wash, soft cyan ring, inner
   glow, Light label; hover fades in a cyan wash). */
.ri-btn--secondary {
  --_btn-bg: var(--ri-gradient-glass, linear-gradient(180deg, rgba(255, 255, 255, 0.1) 0%, rgba(255, 255, 255, 0) 100%));
  --_btn-ring: linear-gradient(90deg, rgba(121, 241, 255, 0.45) 0.01%, rgba(56, 230, 215, 0.45) 50.01%, rgba(56, 230, 215, 0.45) 100%);
  --_btn-shadow: inset 0 -6px 15px rgba(56, 230, 215, 0.25), inset 0 6px 15px rgba(121, 241, 255, 0.25);
  --_btn-wash: linear-gradient(90deg, rgba(46, 209, 244, 0.12), rgba(46, 209, 244, 0.05));
  --_btn-fw: var(--ri-weight-light, 300);
}

/* Neutral: the secondary ("Sign out") construction in grey, for a form's
   Cancel next to a coloured action (owner 2026-10-05: "the sign out button
   of ours, styled grey"). Glass wash, a soft white ring, a white inner glow,
   white Light label; hover fades in a white wash. */
.ri-btn--neutral {
  --_btn-bg: var(--ri-gradient-glass, linear-gradient(180deg, rgba(255, 255, 255, 0.1) 0%, rgba(255, 255, 255, 0) 100%));
  --_btn-ring: linear-gradient(90deg, rgba(255, 255, 255, 0.34) 0.01%, rgba(214, 219, 228, 0.26) 50.01%, rgba(214, 219, 228, 0.26) 100%);
  --_btn-shadow: inset 0 -6px 15px rgba(214, 219, 228, 0.1), inset 0 6px 15px rgba(255, 255, 255, 0.1);
  --_btn-wash: linear-gradient(90deg, rgba(255, 255, 255, 0.1), rgba(255, 255, 255, 0.04));
  --_btn-color: #fff;
  --_btn-fw: var(--ri-weight-light, 300);
}

/* Ghost: the secondary without its chrome, for low-emphasis actions
   (Cancel, Not now, Reset). */
.ri-btn--ghost {
  --_btn-color: var(--ri-text-muted, rgba(255, 255, 255, 0.74));
  --_btn-wash: linear-gradient(90deg, rgba(46, 209, 244, 0.12), rgba(46, 209, 244, 0.05));
  --_btn-fw: var(--ri-weight-light, 300);
}
@media (hover: hover) {
  .ri-btn--ghost:hover { --_btn-color: #fff; }
}

/* Close: the one close / dismiss X for every dialog, popup, sheet, card,
   banner, toast and form (owner 2026-10-06, confirmed 2026-10-09: "use this
   'x' styling for ALL our x's"). Markup: class="ri-btn ri-btn--ghost
   ri-btn--icon ri-btn--sm ri-btn--close" with the 16 px X (path
   M6 18L18 6M6 6l12 12, stroke 2, round caps). At rest it shows the ghost
   hover wash and a white X; the wash is the button's own background, so it
   also shows on touch screens, where ::after is the 44 px hit area. Hover
   (fine pointers) fades a second cyan layer over it (::after); nothing
   moves. Page CSS sets only its place (position, top/right, z-index, margin)
   and transitions. */
.ri-btn--close {
  --_btn-h: var(--ri-control-sm, 32px);
  --_btn-icon: var(--ri-icon-sm, 16px);
  --_btn-radius: var(--ri-radius-sm, 8px);
  --_btn-color: #fff;
  --_btn-bg: linear-gradient(90deg, rgba(46, 209, 244, 0.12), rgba(46, 209, 244, 0.05));
  --_btn-wash: linear-gradient(90deg, rgba(46, 209, 244, 0.09), rgba(46, 209, 244, 0.045));
}
/* Two classes, so an older container rule (e.g. dashboard-embed.css
   ".shoe-tag button { background: none; color: cyan; padding: 0 }") or a
   page's own close class cannot resize, strip or recolour it. */
.ri-btn.ri-btn--close {
  flex: none;
  box-sizing: border-box;
  width: var(--_btn-h);
  min-width: var(--_btn-h);
  height: var(--_btn-h);
  min-height: var(--_btn-h);
  padding: 0;
  border: 0;
  border-radius: var(--_btn-radius);
  background: var(--_btn-bg);
  color: var(--_btn-color);
  font-size: var(--_btn-fs);
  line-height: 1;
}
.ri-btn.ri-btn--close:hover,
.ri-btn.ri-btn--close:active,
.ri-btn.ri-btn--close:focus {
  background: var(--_btn-bg);
  color: var(--_btn-color);
  transform: none;
}
.ri-btn.ri-btn--close > svg {
  width: var(--_btn-icon);
  height: var(--_btn-icon);
  margin: 0;
  color: inherit;
  stroke: currentColor;
  stroke-width: 2;
  stroke-linecap: round;
  stroke-linejoin: round;
  /* A two-colour sprite X, if one is ever left in, draws white too. */
  --icon-muted: currentColor;
  --icon-accent: currentColor;
}

/* Danger: destructive actions (Delete account, Delete report, Disconnect).
   The secondary button in red (owner 2026-10-04): the same glass wash, a red
   gradient ring, a red inner glow and a red wash on hover; the label is white
   Light. */
.ri-btn--danger {
  --_btn-bg: var(--ri-gradient-glass, linear-gradient(180deg, rgba(255, 255, 255, 0.1) 0%, rgba(255, 255, 255, 0) 100%));
  --_btn-ring: linear-gradient(90deg, rgba(252, 165, 165, 0.5) 0.01%, rgba(248, 113, 113, 0.45) 50.01%, rgba(248, 113, 113, 0.45) 100%);
  --_btn-shadow: inset 0 -6px 15px rgba(248, 113, 113, 0.22), inset 0 6px 15px rgba(252, 165, 165, 0.2);
  --_btn-wash: linear-gradient(90deg, rgba(248, 113, 113, 0.14), rgba(248, 113, 113, 0.06));
  --_btn-color: #fff;
  --_btn-fw: var(--ri-weight-light, 300);
}

/* Success: saving actions (Drift Inspector "Save windows", owner 2026-10-05).
   The secondary button in green: glass wash, a green gradient ring, a green
   inner glow and a green wash on hover; the label is white Light. */
.ri-btn--success {
  --_btn-bg: var(--ri-gradient-glass, linear-gradient(180deg, rgba(255, 255, 255, 0.1) 0%, rgba(255, 255, 255, 0) 100%));
  --_btn-ring: linear-gradient(90deg, rgba(110, 231, 183, 0.6) 0.01%, rgba(52, 211, 153, 0.55) 50.01%, rgba(52, 211, 153, 0.55) 100%);
  --_btn-shadow: inset 0 -6px 15px rgba(52, 211, 153, 0.22), inset 0 6px 15px rgba(110, 231, 183, 0.18);
  --_btn-wash: linear-gradient(90deg, rgba(52, 211, 153, 0.16), rgba(52, 211, 153, 0.06));
  --_btn-color: #fff;
  --_btn-fw: var(--ri-weight-light, 300);
}

/* Strava: Strava actions (Sync shoe in the activity logs, owner 2026-10-05).
   The secondary button in Strava orange: glass wash, an orange gradient ring,
   an orange inner glow and an orange wash on hover; the label is white Light. */
.ri-btn--strava {
  --_btn-bg: var(--ri-gradient-glass, linear-gradient(180deg, rgba(255, 255, 255, 0.1) 0%, rgba(255, 255, 255, 0) 100%));
  --_btn-ring: linear-gradient(90deg, rgba(255, 154, 104, 0.6) 0.01%, rgba(252, 76, 2, 0.55) 50.01%, rgba(252, 76, 2, 0.55) 100%);
  --_btn-shadow: inset 0 -6px 15px rgba(252, 76, 2, 0.22), inset 0 6px 15px rgba(255, 154, 104, 0.18);
  --_btn-wash: linear-gradient(90deg, rgba(252, 76, 2, 0.16), rgba(252, 76, 2, 0.06));
  --_btn-color: #fff;
  --_btn-fw: var(--ri-weight-light, 300);
}

/* Strava full: the primary in Strava orange (owner 2026-10-07) for the first
   Strava connect (Profile "Connect with Strava", the Runs page's Strava
   button before anything has synced; Sync now stays glass). Same size, radius,
   weight, white label, inset shade and hover as the primary; the gradient
   runs from the Strava ring's light orange to Strava orange. */
.ri-btn--strava-full {
  --_btn-bg: linear-gradient(180deg, #FF9A68 -11.11%, #FC4C02 100%);
  --_btn-shadow: inset 0 -3px 10px rgba(0, 0, 0, 0.25);
  --_btn-fw: var(--ri-weight-medium, 500);
}
@media (hover: hover) {
  .ri-btn--strava-full:hover { opacity: 0.8; }
}

/* Lime: the Contributor CTA, run-it.io's "Contribute & get free Pro ->". Its
   edge is lime only: the cyan gradient ring inside it read as a stray cyan
   border (owner 2026-10-12, Pro page "weird button styling"). */
.ri-btn--lime {
  --_btn-bg: var(--ri-lime, #B7FF00);
  --_btn-bw: 1px;
  --_btn-border: var(--ri-lime-edge, #DFFF8F);
  --_btn-ring: none;
  --_btn-color: var(--ri-lime-ink, #101A0B);
  --_btn-shadow: 0 0 24px rgba(183, 255, 0, 0.22), -10px 0 34px rgba(46, 209, 244, 0.16), 10px 0 34px rgba(139, 92, 246, 0.18), inset 0 1px 0 rgba(255, 255, 255, 0.34);
  --_btn-fw: var(--ri-weight-medium, 500);
}
@media (hover: hover) {
  .ri-btn--lime:hover {
    --_btn-bg: var(--ri-lime-hover, #CAFF4D);
    --_btn-shadow: 0 0 30px rgba(183, 255, 0, 0.28), -12px 0 38px rgba(46, 209, 244, 0.2), 12px 0 38px rgba(139, 92, 246, 0.22), inset 0 1px 0 rgba(255, 255, 255, 0.4);
  }
}

/* Contributor: the Dashboard's Contributor entry as a button (owner
   2026-10-04): a lime label inside the brand-gradient ring (cyan, blue,
   purple, lime). Hover brightens the ring and lights a lime and purple glow
   inside the button; nothing moves and nothing glows outside it. */
.ri-btn--contributor {
  --_btn-bg: transparent;
  --_btn-color: var(--ri-lime, #B7FF00);
  --_btn-ring: linear-gradient(105deg, #2ED1F4 0%, #4B7CFF 26%, #8B5CF6 50%, #B7FF00 76%, #2ED1F4 100%);
  --_btn-fw: var(--ri-weight-semibold, 600);
}
@media (hover: hover) {
  .ri-btn--contributor:hover {
    --_btn-bg: linear-gradient(180deg, rgba(139, 92, 246, 0.1), rgba(183, 255, 0, 0.05));
    --_btn-shadow: rgba(183, 255, 0, 0.22) 0 -6px 15px inset, rgba(139, 92, 246, 0.28) 0 6px 15px inset;
  }
  .ri-btn--contributor:hover::before { filter: brightness(1.18) saturate(1.08); }
}
.ri-btn--contributor:focus-visible {
  outline: 2px solid var(--ri-lime-edge, #DFFF8F);
  outline-offset: 2px;
}

/* Link: "View ..." navigation. The one link motion of the Dashboard: the
   label stays white and the cyan arrow (<span class="dash-cta-arrow">, as
   dashboardCtaLabelMarkup writes it; owner 2026-10-04) eases 3 px; .is-up
   eases upward. Type: the secondary role (14 / 13 px, SPEC v2). */
.ri-btn--link {
  --_btn-h: auto;
  --_btn-px: 0px;
  --_btn-fs: var(--ri-text-secondary, 14px);
  --_btn-lh: 20px;
  --_btn-fw: var(--ri-weight-light, 300);
  --_btn-radius: 4px;
  gap: 0.3em;
}
.ri-btn--link .dash-cta-arrow {
  display: inline-block;
  color: var(--ri-cyan, #2ED1F4);
  transition: transform var(--ri-dur-fast, 150ms) var(--ri-ease-standard, cubic-bezier(0.4, 0, 0.2, 1));
}
.ri-btn--link:hover .dash-cta-arrow,
.ri-btn--link:focus-visible .dash-cta-arrow { transform: translateX(3px); }
.ri-btn--link:hover .dash-cta-arrow.is-up,
.ri-btn--link:focus-visible .dash-cta-arrow.is-up { transform: translateY(-3px); }

/* Select: the My Shoes race-distance pill ("5k 10K 21k 42k"). A glass pill
   with a soft slate edge and inner glow; hover and selected light a gradient
   ring and glow in the accent (the app cyan; .ri-btn--race gives the My Shoes
   rose). Label at the 12 px floor (the pill was 9-10.5 px). */
.ri-btn--select {
  --_sel-1: 121, 241, 255;
  --_sel-2: 46, 209, 244;
  --_sel-3: 0, 194, 255;
  --_btn-radius: var(--ri-radius-pill, 999px);
  --_btn-bw: 1px;
  --_btn-bg: var(--ri-gradient-glass, linear-gradient(180deg, rgba(255, 255, 255, 0.1) 0%, rgba(255, 255, 255, 0) 100%));
  --_btn-color: rgba(255, 255, 255, 0.58);
  --_btn-fw: var(--ri-weight-regular, 400);
  --_btn-shadow: inset 0 0 0 1px rgba(148, 163, 184, 0.24), inset 0 -6px 15px rgba(148, 163, 184, 0.12), inset 0 6px 15px rgba(255, 255, 255, 0.06);
  --_btn-ring: linear-gradient(90deg, rgba(var(--_sel-1), 0.78) 0.01%, rgba(var(--_sel-2), 0.84) 50.01%, rgba(var(--_sel-3), 0.78) 100%);
  --_btn-ring-opacity: 0;
}
/* Squared select: the same chip with the field radius, for filter rows that
   sit beside date and select fields (owner, 2026-10-03). */
.ri-btn--select.ri-btn--square { --_btn-radius: var(--ri-radius-sm, 8px); }
.ri-btn--select.ri-btn--sm { --_btn-fs: var(--ri-text-pill, 12px); }
.ri-btn--race {
  --_sel-1: 255, 77, 109;
  --_sel-2: 225, 29, 72;
  --_sel-3: 244, 63, 94;
}
/* Daily: the run type's blue (owner 2026-10-05: "these buttons should also be blue"). */
.ri-btn--daily {
  --_sel-1: 147, 197, 253;
  --_sel-2: 59, 130, 246;
  --_sel-3: 96, 165, 250;
}
/* Tempo: the run type's orange (owner 2026-10-04: "tempo needs to be orange"). */
.ri-btn--tempo {
  --_sel-1: 253, 186, 116;
  --_sel-2: 249, 115, 22;
  --_sel-3: 251, 146, 60;
}
@media (hover: hover) {
  .ri-btn--select:hover {
    --_btn-color: #fff;
    --_btn-ring-opacity: 0.8;
    --_btn-shadow: inset 0 -6px 17px rgba(var(--_sel-2), 0.24), inset 0 6px 17px rgba(var(--_sel-1), 0.2);
  }
}
.ri-btn--select:is([aria-pressed="true"], [aria-selected="true"], [aria-checked="true"], .is-selected) {
  --_btn-color: #fff;
  --_btn-ring-opacity: 1;
  --_btn-shadow: inset 0 -6px 17px rgba(var(--_sel-2), 0.34), inset 0 6px 17px rgba(var(--_sel-1), 0.28), 0 0 0 1px rgba(var(--_sel-2), 0.12);
}

/* A row of select pills: filters, chips, tabs, source toggles, segmented
   controls. role="group" + aria-pressed, or role="tablist"/"radiogroup". */
.ri-select-group {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 4px;
}
.ri-select-group--fill > .ri-btn { flex: 1 1 0; }
@media (max-width: 640px) {
  .ri-select-group { gap: 6px; }
}

/* --------------------------------------------------------------------------
   Card carousel dots (owner 2026-10-10: every card carousel uses the Shoe
   Analysis "Choose your run focus" dots). A 10 px grey dot, 12 px apart; the
   current card's dot is a 26 x 10 cyan pill. Each button is a 24 px tall hit
   area around its dot; the page sets the row's margin so the dots sit 20 px
   under the cards (--ri-carousel-dots-gap is that distance; the row's top is
   7 px above the dots).
   <div class="ri-carousel-dots" role="group" aria-label="…">
     <button type="button" class="ri-carousel-dot" aria-current="true|false"
       (or aria-pressed, or .is-active) aria-label="…"></button> … </div>
   -------------------------------------------------------------------------- */
.ri-carousel-dots {
  --ri-carousel-dots-gap: 20px;
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 0;
}
.ri-carousel-dots[hidden] { display: none; }
.ri-carousel-dot {
  display: grid;
  place-items: center;
  width: auto;
  min-width: 0;
  height: 24px;
  margin: 0;
  padding: 0 6px;
  border: 0;
  border-radius: 999px;
  background: none;
  box-shadow: none;
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
}
.ri-carousel-dot::before {
  content: "";
  display: block;
  width: 10px;
  height: 10px;
  border-radius: 999px;
  background: rgba(255, 255, 255, 0.24);
  transition:
    width 320ms var(--ri-ease-standard, cubic-bezier(0.4, 0, 0.2, 1)),
    background-color 320ms var(--ri-ease-standard, cubic-bezier(0.4, 0, 0.2, 1));
}
.ri-carousel-dot:is([aria-current="true"], [aria-pressed="true"], .is-active)::before {
  width: 26px;
  background: var(--ri-cyan, #2ED1F4);
}
.ri-carousel-dot:focus-visible { outline: none; }
.ri-carousel-dot:focus-visible::before {
  outline: 2px solid var(--ri-cyan, #2ED1F4);
  outline-offset: 3px;
}
@media (prefers-reduced-motion: reduce) {
  .ri-carousel-dot::before { transition: none; }
}

.ri-btn-group {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--ri-space-2, 8px);
}

/* --------------------------------------------------------------------------
   Page title block: the Dashboard h1 (40/300, 2 px cyan underline 130 % wide
   with a glow; 28 px on phones with the fading underline) and its lead line.
   <div class="ri-page-head">
     <div class="ri-page-head-copy"><h1 class="ri-page-title">…</h1>
       <p class="ri-page-lead">…</p></div>
     <div class="ri-page-actions">…</div></div>
   The underline scales in once (--ri-dur-enter) after --ri-page-title-delay
   (80 ms); a page with its own entrance sets the delay on an ancestor (Pro
   Welcome: 1s), and a page that reveals late can hold it with
   --ri-page-title-play: paused.
   -------------------------------------------------------------------------- */
.ri-page-head {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: var(--ri-space-4, 16px);
  margin: 0 0 var(--ri-page-head-gap, 48px);
  padding: 0;
}
.ri-page-head-copy {
  flex: 1 1 auto;
  min-width: 0;
}
.ri-page-actions {
  display: flex;
  flex: none;
  align-items: center;
  gap: var(--ri-space-2, 8px);
}
.ri-page-title {
  position: relative;
  display: inline-block;
  max-width: 100%;
  margin: 0;
  padding: 0 0 var(--ri-title-underline-gap, 12px);
  color: #fff;
  font-family: var(--ri-font-sans, 'Noto Sans', ui-sans-serif, system-ui, sans-serif);
  font-size: var(--ri-text-display, 40px);
  font-weight: var(--ri-weight-light, 300);
  line-height: var(--ri-leading-display, 1.1);
  letter-spacing: var(--ri-tracking-display, -0.005em);
  text-shadow: none;
  text-transform: none;
  animation: none;
}
.ri-page-title::before { content: none; }
.ri-page-title::after {
  content: "";
  display: block;
  position: absolute;
  top: auto;
  right: auto;
  bottom: 0;
  left: 0;
  width: 130%;
  max-width: calc(100vw - 2 * var(--ri-page-gutter, 32px));
  height: 2px;
  margin: 0;
  opacity: 1;
  background: linear-gradient(90deg, #2ED1F4 0%, #2ED1F4 60%, transparent 100%);
  box-shadow: 0 0 8px rgba(46, 209, 244, 0.4);
  transform-origin: left center;
  transition: none;
  animation: ri-page-underline-in var(--ri-dur-enter, 420ms) var(--ri-ease-out, cubic-bezier(0.22, 1, 0.36, 1)) var(--ri-page-title-delay, 80ms) both;
  animation-play-state: var(--ri-page-title-play, running);
}
@keyframes ri-page-underline-in {
  from { transform: scaleX(0); opacity: 0; }
  to { transform: scaleX(1); opacity: 1; }
}
.ri-page-lead {
  max-width: 46rem;
  margin: var(--ri-title-lead-gap, 12px) 0 0;
  color: var(--ri-text-muted, rgba(255, 255, 255, 0.74));
  font-family: var(--ri-font-sans, 'Noto Sans', ui-sans-serif, system-ui, sans-serif);
  font-size: var(--ri-text-lead, 16px);
  font-weight: var(--ri-weight-light, 300);
  line-height: var(--ri-leading-lead, 1.4);
  letter-spacing: 0;
}
@media (max-width: 640px) {
  .ri-page-head { gap: var(--ri-space-3, 12px); }
  /* The source switcher (Garmin / Stryd / Strava / Apple) gets its own line
     under the subtitle, so the 36 px title has the full width (owner
     2026-10-06: "move the button"). */
  .ri-page-head:has(> .ri-page-actions .profile-toggle-wrap) {
    flex-wrap: wrap;
    row-gap: var(--ri-space-4, 16px);
  }
  .ri-page-head:has(> .ri-page-actions .profile-toggle-wrap) > .ri-page-head-copy { flex-basis: 100%; }
  .ri-page-head:has(> .ri-page-actions .profile-toggle-wrap) > .ri-page-actions {
    align-items: flex-start;
    justify-content: flex-start;
  }
  .ri-page-title::after {
    width: 100%;
    background: linear-gradient(90deg, #2ED1F4 0%, rgba(46, 209, 244, 0.88) 54%, rgba(46, 209, 244, 0.28) 82%, transparent 100%);
    box-shadow: 0 0 14px rgba(46, 209, 244, 0.32), 0 0 24px rgba(46, 209, 244, 0.12);
  }
  .ri-page-lead {
    color: var(--ri-text-faint, rgba(255, 255, 255, 0.62));
    letter-spacing: -0.01em;
  }
}

/* --------------------------------------------------------------------------
   Sections: the Dashboard section card (.dash-card) with its header
   (20 px cyan icon + title + optional top-right stat, then the cyan hairline).
   <section class="ri-card" aria-labelledby="t">
     <header class="ri-card-head"><svg class="ri-card-icon" …/>
       <h2 class="ri-section-title" id="t">…</h2>
       <div class="ri-card-meta">…</div></header>
     …content… </section>
   -------------------------------------------------------------------------- */
.ri-sections {
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  gap: var(--ri-section-gap, 40px);
}
@media (min-width: 1024px) {
  .ri-sections--2col { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}
.ri-span-full { grid-column: 1 / -1; }

.ri-card {
  position: relative;
  box-sizing: border-box;
  min-width: 0;
  margin: 0;
  padding: var(--ri-card-pad-y, 24px) var(--ri-card-pad-x, 24px);
  border: 1px solid var(--ri-border-card, rgba(255, 255, 255, 0.18));
  border-radius: var(--ri-card-radius, 20px);
  background: var(--ri-surface-card, linear-gradient(135deg, rgba(255, 255, 255, 0.05), rgba(255, 255, 255, 0.02)));
  box-shadow: var(--ri-shadow-card, 0 8px 32px rgba(0, 0, 0, 0.1), 0 0 30px rgba(200, 200, 200, 0.07), inset 0 1px 0 rgba(255, 255, 255, 0.1));
  backdrop-filter: none;
  -webkit-backdrop-filter: none;
}
/* Phones: main section cards reach 8 px from the screen edge, past the
   page's 16 px gutter, rounded and bordered, exactly as production's
   Dashboard (owner 2026-10-04, and again 2026-10-05: "this is the width ...
   NOT just full width"). Cards inside cards and dialogs keep their place. */
@media (max-width: 640px) {
  .ri-card:not(.ri-card .ri-card):not(.ri-dialog .ri-card) {
    width: calc(100% + 1rem);
    margin-right: -0.5rem;
    margin-left: -0.5rem;
  }
}
/* The hairline is the head's own bottom background line, so it can never be
   left out or drawn in a second cyan. The row never wraps: the title shrinks
   and the stat stays top right (keep it compact on phones). */
.ri-card-head {
  display: flex;
  flex-wrap: nowrap;
  align-items: center;
  gap: var(--ri-space-2, 8px);
  margin: 0 0 var(--ri-card-body-gap, 16px);
  padding: 0 0 var(--ri-card-head-gap, 16px);
  background: var(--ri-hairline, linear-gradient(90deg, rgba(46, 209, 244, 0.4) 0%, rgba(46, 209, 244, 0.2) 50%, transparent 100%)) left bottom / 100% 1px no-repeat;
}
.ri-card-icon {
  flex: none;
  width: var(--ri-icon-md, 20px);
  height: var(--ri-icon-md, 20px);
  color: var(--ri-cyan, #2ED1F4);
}
.ri-section-title,
.ri-card-title {
  margin: 0;
  color: var(--ri-text-strong, rgba(255, 255, 255, 0.96));
  font-family: var(--ri-font-sans, 'Noto Sans', ui-sans-serif, system-ui, sans-serif);
  font-weight: var(--ri-weight-light, 300);
  text-shadow: none;
  text-transform: none;
  animation: none;
}
.ri-section-title::before,
.ri-section-title::after,
.ri-card-title::before,
.ri-card-title::after { content: none; }
.ri-section-title {
  flex: 0 1 auto;
  min-width: 0;
  font-size: var(--ri-text-title, 24px);
  line-height: var(--ri-leading-title, 1.25);
  letter-spacing: var(--ri-tracking-title, -0.01em);
}
/* A title inside a section or tile (h3). */
.ri-card-title {
  font-size: var(--ri-text-subtitle, 18px);
  line-height: var(--ri-leading-subtitle, 1.3);
  letter-spacing: 0;
}
/* Card-header stats stay top right, on phones too. */
.ri-card-meta {
  flex: none;
  margin-left: auto;
  white-space: nowrap;
  color: var(--ri-text-faint, rgba(255, 255, 255, 0.62));
  font-size: var(--ri-text-caption, 13px);
  line-height: var(--ri-leading-caption, 1.35);
  font-variant-numeric: var(--ri-numeric, tabular-nums);
  text-align: right;
}

/* Tile: a filled box inside a section card; no second border. No inner glow:
   shoe, report and run cards use it (owner 2026-10-05). */
.ri-tile {
  box-sizing: border-box;
  min-width: 0;
  padding: var(--ri-tile-pad, 16px);
  border: 0;
  border-radius: var(--ri-radius-md, 12px);
  background: var(--ri-surface-tile, rgba(255, 255, 255, 0.04));
  color: inherit;
}
a.ri-tile,
button.ri-tile {
  display: block;
  width: 100%;
  text-align: inherit;
  text-decoration: none;
  cursor: pointer;
  font: inherit;
  transition: background-color var(--ri-dur-fast, 150ms) var(--ri-ease-standard, cubic-bezier(0.4, 0, 0.2, 1));
}

/* The tile (owner 2026-10-07: S1, "like our shoe cards"): the tile with a
   soft edge and a little light at its bottom edge in its own colour,
   --tile-color (a run type, a metric's condition, a check's result; cyan by
   default). Hover or choose it and a stronger light grows up from the bottom,
   slowly and after a short pause, and the edge fills with the colour: the
   Shoe Analysis tile motion (#shoeList .rec-tile, 700/800 ms after 140 ms in,
   320/380 ms out). The light is drawn in the tile's own background (no
   pseudo-elements, which several tiles already use) and grows with
   --tile-lift. Pages whose old rules out-rank it draw it from --_tile-bg and
   --_tile-shadow with !important (biomech-form-efficiency.css). */
@property --tile-lift {
  syntax: "<number>";
  inherits: false;
  initial-value: 0;
}
.ri-tile--glow {
  --tile-color: var(--ri-cyan, #2ED1F4);
  --_tile-edge: 18%;
  --_tile-bg:
    radial-gradient(130% calc(22% + 53% * var(--tile-lift)) at 50% 104%, color-mix(in srgb, var(--tile-color) calc(50% * var(--tile-lift)), transparent), color-mix(in srgb, var(--tile-color) calc(17% * var(--tile-lift)), transparent) 50%, transparent 78%),
    linear-gradient(0deg, color-mix(in srgb, var(--tile-color) calc(10% * var(--tile-lift)), transparent), transparent 70%),
    radial-gradient(120% 55% at 50% 108%, color-mix(in srgb, var(--tile-color) 20%, transparent), color-mix(in srgb, var(--tile-color) 5%, transparent) 55%, transparent 80%),
    var(--ri-surface-tile, rgba(255, 255, 255, 0.04));
  --_tile-shadow: inset 0 0 0 1px color-mix(in srgb, var(--tile-color) var(--_tile-edge), transparent), inset 0 0 16px color-mix(in srgb, var(--tile-color) calc(6% + 6% * var(--tile-lift)), transparent);
  --_tile-in: --tile-lift 800ms cubic-bezier(0.3, 0.1, 0.3, 1) 140ms, box-shadow 240ms var(--ri-ease-standard, cubic-bezier(0.4, 0, 0.2, 1)), opacity 240ms ease, filter 240ms ease, transform 180ms ease-out;
  --_tile-out: --tile-lift 380ms var(--ri-ease-standard, cubic-bezier(0.4, 0, 0.2, 1)), box-shadow 240ms var(--ri-ease-standard, cubic-bezier(0.4, 0, 0.2, 1)), opacity 240ms ease, filter 240ms ease, transform 180ms ease-out;
  background: var(--_tile-bg);
  box-shadow: var(--_tile-shadow);
  transition: var(--_tile-out);
}
/* Only a tile that does something on hover (a control, a link, a tooltip)
   lights up under the pointer; a plain readout stays still. */
@media (hover: hover) {
  .ri-tile--glow:is(a, button, [role="button"], [tabindex], [data-tip]):hover {
    --tile-lift: 1;
    --_tile-edge: 45%;
    transition: var(--_tile-in);
  }
}
.ri-tile--glow:is(.is-selected, .is-active, [aria-pressed="true"], [aria-selected="true"]) {
  --tile-lift: 1;
  --_tile-edge: 100%;
  transition: var(--_tile-in);
}
@media (prefers-reduced-motion: reduce) {
  .ri-tile--glow,
  .ri-tile--glow:is(:hover, .is-selected, .is-active, [aria-pressed="true"], [aria-selected="true"]) {
    transition: box-shadow 240ms linear, opacity 240ms linear, filter 240ms linear;
  }
}
@media (hover: hover) {
  a.ri-tile:hover,
  button.ri-tile:hover { background: var(--ri-surface-tile-hover, rgba(255, 255, 255, 0.07)); }
}
a.ri-tile:focus-visible,
button.ri-tile:focus-visible {
  outline: 2px solid var(--ri-cyan, #2ED1F4);
  outline-offset: 2px;
}
.ri-tiles {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(160px, 1fr));
  gap: var(--ri-space-3, 12px);
}
@media (max-width: 640px) {
  .ri-tiles {
    grid-template-columns: repeat(2, minmax(0, 1fr));
    gap: var(--ri-space-2, 8px);
  }
}

/* --------------------------------------------------------------------------
   Media tile: one shoe (or product) on a tile, the same on every page (audit
   V3-02, V3-16; reference: the Report's shoe tile). A 4:3 picture box, the
   name on up to two lines (both lines reserved, so rows line up), optional
   pills, 1-3 stat rows (label left, tabular value right), an optional price
   chip and an optional CTA pinned to the bottom. The match or run type is
   said by a pill, never by a tinted tile or a coloured border.
   <article class="ri-tile ri-media-tile">
     <div class="ri-media-tile-pills"><span class="ri-tag ri-tag--match-great">Great match</span></div>
     <div class="ri-media-tile-media"><img src="…" alt="…" width="240" height="180" loading="lazy"></div>
     <p class="ri-media-tile-title">Nike Vomero 18</p>
     <span class="ri-media-tile-price ri-num">€150</span>
     <dl class="ri-media-tile-stats">
       <div class="ri-media-tile-stat"><dt>LPI est.</dt><dd class="ri-num">41–54</dd></div>
     </dl>
     <p class="ri-media-tile-notes">Cushioned · Daily</p>
     <div class="ri-media-tile-action"><button type="button" class="ri-btn ri-btn--cta ri-btn--sm ri-btn--block">View retailers …</button></div>
   </article>
   .ri-media-tiles is the grid: rows that fill from 200 px; 2 a row on phones.
   -------------------------------------------------------------------------- */
.ri-media-tiles {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
  gap: var(--ri-space-4, 16px);
}
.ri-media-tile,
a.ri-tile.ri-media-tile,
button.ri-tile.ri-media-tile {
  position: relative;
  display: flex;
  flex-direction: column;
  gap: var(--ri-space-3, 12px);
  text-align: left;
}
.ri-media-tile-pills {
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
}
.ri-media-tile-media {
  display: flex;
  flex: none;
  align-items: center;
  justify-content: center;
  aspect-ratio: 4 / 3;
  overflow: hidden;
  border-radius: var(--ri-radius-sm, 8px);
  color: rgba(255, 255, 255, 0.2);
  isolation: isolate;
}
.ri-media-tile-media > img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: contain;
}
.ri-media-tile-media > svg {
  width: 64px;
  height: auto;
}
.ri-media-tile-title {
  display: -webkit-box;
  min-height: calc(2em * var(--ri-leading-subtitle, 1.3));
  margin: 0;
  overflow: hidden;
  color: var(--ri-text-strong, rgba(255, 255, 255, 0.96));
  font-size: var(--ri-text-subtitle, 18px);
  font-weight: var(--ri-weight-semibold, 600);
  font-variant-numeric: var(--ri-numeric, tabular-nums);
  line-height: var(--ri-leading-subtitle, 1.3);
  -webkit-box-orient: vertical;
  -webkit-line-clamp: 2;
  line-clamp: 2;
}
.ri-media-tile-price {
  box-sizing: border-box;
  display: inline-flex;
  align-items: center;
  align-self: flex-start;
  height: 22px;
  padding: 0 8px;
  border-radius: var(--ri-radius-pill, 999px);
  background: rgba(255, 255, 255, 0.06);
  color: var(--ri-text-strong, rgba(255, 255, 255, 0.96));
  font-size: var(--ri-text-pill, 12px);
  font-weight: var(--ri-weight-semibold, 600);
  font-variant-numeric: var(--ri-numeric, tabular-nums);
  line-height: 1;
  white-space: nowrap;
}
.ri-media-tile-stats {
  display: grid;
  gap: 4px;
  margin: 0;
}
.ri-media-tile-stat {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  justify-content: space-between;
  column-gap: var(--ri-space-2, 8px);
  row-gap: 2px;
}
.ri-media-tile-stat dt {
  color: var(--ri-text-faint, rgba(255, 255, 255, 0.62));
  font-size: var(--ri-text-caption, 13px);
  line-height: var(--ri-leading-caption, 1.35);
}
.ri-media-tile-stat dd {
  margin: 0;
  color: var(--ri-text-strong, rgba(255, 255, 255, 0.96));
  font-size: var(--ri-text-metric-sm, 13px);
  font-weight: var(--ri-weight-semibold, 600);
  font-variant-numeric: var(--ri-numeric, tabular-nums);
  line-height: var(--ri-leading-caption, 1.35);
}
.ri-media-tile-notes {
  display: -webkit-box;
  margin: 0;
  overflow: hidden;
  color: var(--ri-text-faint, rgba(255, 255, 255, 0.62));
  font-size: var(--ri-text-label, 12px);
  line-height: var(--ri-leading-label, 1.33);
  -webkit-box-orient: vertical;
  -webkit-line-clamp: 2;
  line-clamp: 2;
}
.ri-media-tile-action {
  margin-top: auto;
}
.ri-media-tile-action .ri-btn {
  --_btn-px: 10px;
}
@media (max-width: 640px) {
  .ri-media-tiles {
    grid-template-columns: repeat(2, minmax(0, 1fr));
    gap: var(--ri-space-2, 8px);
  }
}

/* --------------------------------------------------------------------------
   Loading kit (owner picks 2026-10-09): one look on every page. Use these,
   never a page-specific loader:
   - First load of a known layout: .ri-skeleton blocks drawn in the final
     layout (.ri-skeleton--text for a line of text). Swap them for the content
     with RunItMotion.revealFromSkeleton: each block turns into its own element
     in place, coming into focus (blur 6 px -> sharp, 560 ms, top to bottom).
   - A refresh that fetches: the same skeleton again, then the same reveal.
   - A pressed button: aria-busy="true" + disabled. The Relay spinner takes
     the icon's place (or the label's, without an icon) in the button's own
     colour: white on a primary, red on a danger, green on a success.
   - A long job with a count: the sync strip (js/sync-progress-animation.js).
   - Unknown shape (a popup, a tooltip): the Relay spinner, .ri-loader.
   - Failed or timed out: the blocks stop and dim (.is-failed, or
     [data-load-state="failed"] on a container) and a small .ri-load-error
     note sits top right with Retry (RunItMotion.loadFailed).
   - Timing: a skeleton stays invisible for its first 300 ms, so a fast load
     never flashes one; once seen it stays at least 400 ms (revealFromSkeleton
     holds the reveal; RunItMotion.skeletonHoldMs for custom swaps).
   - Shoe images keep their small pulse bar.
   -------------------------------------------------------------------------- */

/* Skeleton (owner pick 1D, made light 2026-10-09): a faint block in the final
   layout (white .05, the element's own radius). With a mouse its cyan light
   gently swells and fades (only the opacity of its ::after moves); on phones
   and touch screens the block stays still. Every moving light measured
   (a background sweep, a sliding band, a pulse) cost a phone 0.5 to 1 s of
   main thread per page load and made carousels stutter; still blocks cost
   nothing. Its text stays transparent, so revealFromSkeleton ghosts it as one
   block. <span class="ri-skeleton ri-skeleton--text">000 km</span> */
.ri-skeleton {
  border-color: transparent;
  background-color: rgba(255, 255, 255, 0.05);
  background-image: none;
  color: transparent;
  pointer-events: none;
  user-select: none;
  animation: ri-skeleton-wait var(--ri-skeleton-delay, 300ms) linear;
}
@media (hover: hover) and (pointer: fine) and (prefers-reduced-motion: no-preference) {
  /* Zero specificity: a page that places its block (absolute, sticky) keeps it. */
  :where(.ri-skeleton) { position: relative; }
  .ri-skeleton::after {
    content: '';
    position: absolute;
    inset: 0;
    border-radius: inherit;
    /* 16 % at the peak: at 8 % the breath could not be told from a still block
       (owner 2026-10-10: "it doesn't do anything now"). Still opacity-only. */
    background: rgba(46, 209, 244, 0.16);
    opacity: 0;
    animation: ri-skeleton-shine var(--ri-dur-skeleton, 1800ms) ease-in-out infinite;
    pointer-events: none;
  }
}
.ri-skeleton > * { visibility: hidden; }
/* Page rules colour tags, badges and values strongly enough to show the
   placeholder text through a block; the text fill wins over any colour. */
.ri-skeleton,
.ri-skeleton * { -webkit-text-fill-color: transparent; }
/* A line of text (owner pick 2A): the bar is as tall as the letters, not the
   whole line box, so lines keep their air. The element keeps its line box. */
.ri-skeleton--text {
  clip-path: inset(max(0px, calc(50% - 0.39em)) 0 round 4px);
}
@keyframes ri-skeleton-shine {
  0%, 100% { opacity: 0; }
  50% { opacity: 1; }
}
/* Invisible for the first --ri-skeleton-delay (owner pick 10A): data that
   lands sooner never shows a skeleton. */
@keyframes ri-skeleton-wait {
  from, to { opacity: 0; }
}
/* Failed (owner pick 8C): the blocks stop and dim; the note says so. */
.ri-skeleton.is-failed,
[data-load-state="failed"] .ri-skeleton {
  animation: none;
  opacity: 0.45;
}
.ri-skeleton.is-failed::after,
[data-load-state="failed"] .ri-skeleton::after { display: none; }


/* The "Couldn't load · Retry" note (owner pick 8C), top right of the card
   whose content failed. Fades in and out like every message.
   <p class="ri-load-error ri-load-error--corner" role="status"><svg …warning/>
     <span>Couldn't load</span> · <button type="button">Retry</button></p> */
.ri-load-error {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  margin: 0;
  font-size: var(--ri-text-label, 12px);
  font-weight: var(--ri-weight-regular, 400);
  line-height: 1.35;
  color: var(--ri-text-muted, rgba(255, 255, 255, 0.74));
  opacity: 1;
  transition:
    opacity var(--ri-dur-toast-in, 200ms) var(--ri-ease-out, ease-out),
    display var(--ri-dur-toast-in, 200ms) allow-discrete;
}
.ri-load-error:is([hidden], [data-leaving]) {
  opacity: 0;
  transition-duration: var(--ri-dur-toast-out, 160ms);
  transition-timing-function: var(--ri-ease-in, ease-in);
}
.ri-load-error[hidden] { display: none; }
@starting-style {
  .ri-load-error { opacity: 0; }
}
.ri-load-error .run-it-icon {
  width: 16px;
  height: 16px;
  flex: none;
  --icon-accent: #F87171;
  --icon-muted: rgba(248, 113, 113, 0.5);
}
.ri-load-error button {
  margin: 0;
  padding: 0;
  border: 0;
  background: none;
  font: inherit;
  color: var(--ri-cyan, #2ED1F4);
  cursor: pointer;
}
.ri-load-error button:hover { text-decoration: underline; }
.ri-load-error button:focus-visible { outline: 2px solid var(--ri-cyan, #2ED1F4); outline-offset: 2px; border-radius: 2px; }
/* In a card head the note takes the place of the head's own meta (a count, a
   date), which belongs to the content that failed to load. */
[data-load-state="failed"] > .ri-card-head > .ri-card-meta,
[data-load-state="failed"] > * > .ri-card-head > .ri-card-meta { display: none; }
/* Phones: its own right-aligned line at the top of the card. */
.ri-load-error--line {
  display: flex;
  justify-content: flex-end;
  margin: 0 0 var(--ri-space-2, 8px);
}
.ri-load-error--corner {
  position: absolute;
  top: var(--ri-card-pad-y, 24px);
  right: var(--ri-card-pad-x, 24px);
  z-index: 2;
}
@media (prefers-reduced-motion: reduce) {
  .ri-load-error { transition: none; }
}


/* --------------------------------------------------------------------------
   Text roles
   -------------------------------------------------------------------------- */
/* Kicker above a value: the caption role, sentence case (owner 2026-10-05). */
.ri-eyebrow {
  margin: 0;
  color: var(--ri-text-faint, rgba(255, 255, 255, 0.62));
  font-size: var(--ri-text-caption, 13px);
  font-weight: var(--ri-weight-regular, 400);
  line-height: var(--ri-leading-caption, 1.35);
  letter-spacing: 0;
  text-transform: none;
}
/* Tabular figures for metrics, table cells, timers and sliders. */
.ri-num { font-variant-numeric: var(--ri-numeric, tabular-nums); }

/* --------------------------------------------------------------------------
   Tag (read-only label): one look for every status, category, confidence,
   match and required/optional label in the app (owner 2026-10-04, "D1+D3
   glow"): a round 24 px pill, padding 3/10, 13 px Inter Light 300 in white
   0.92; inside, a dark 135deg wash of the tint; the edge, 1 px in the full
   tint; a soft inner glow of the tint. Every variant keeps this geometry and
   type; only --tag-rgb changes.
   <span class="ri-tag ri-tag--daily">Daily</span>
   Variants: neutral (default, slate) · --cyan/--info/--brand/--pro/--long-run ·
   run types --daily --tempo --race --interval · confidence --high --medium
   --low · status --success --warning --danger · match tiers --match-great
   --match-good --match-low · --tint (the page sets --tag-rgb, e.g. the Shoe
   Analysis tile tier). Not for buttons, filter chips or nav badges, and not
   for the filled tags (Dominant pattern, Coming soon, tooltip values), which
   keep their solid fill.
   .ri-pill is the old name of this component and renders the same tag.
   -------------------------------------------------------------------------- */
.ri-tag,
.ri-pill {
  /* Neutral by default: slate. */
  --tag-rgb: 100, 116, 139;
  box-sizing: border-box;
  display: inline-flex;
  align-items: center;
  justify-content: flex-start;
  gap: 6px;
  max-width: 100%;
  height: 24px;
  padding: 3px 10px;
  border: 1px solid rgb(var(--tag-rgb));
  border-radius: 999px;
  background: linear-gradient(135deg, rgba(var(--tag-rgb), 0.18), rgba(255, 255, 255, 0.028) 44%, rgba(7, 15, 28, 0.86));
  /* A soft glow of the tint and a short dark shadow lift the pill off the
     card (owner 2026-10-04); black alone does not read on the dark cards. */
  box-shadow: inset 0 0 10px rgba(var(--tag-rgb), 0.28), 0 0 10px rgba(var(--tag-rgb), 0.24), 0 2px 6px rgba(0, 0, 0, 0.4);
  color: rgba(255, 255, 255, 0.92);
  font-family: var(--ri-font-sans, 'Noto Sans', ui-sans-serif, system-ui, sans-serif);
  font-size: var(--ri-text-caption, 13px);   /* 13 px Light at every width (owner decision) */
  font-style: normal;
  font-weight: var(--ri-weight-light, 300);
  line-height: 16px;
  letter-spacing: 0;
  text-transform: none;
  text-decoration: none;
  white-space: nowrap;
  vertical-align: middle;
  flex-shrink: 0;
}
.ri-tag > svg,
.ri-pill > svg {
  flex: none;
  width: var(--ri-icon-xs, 12px);
  height: var(--ri-icon-xs, 12px);
}
/* Cyan / brand. */
.ri-tag--cyan, .ri-tag--info, .ri-tag--brand, .ri-tag--pro, .ri-tag--long-run,
.ri-pill--info { --tag-rgb: 46, 209, 244; }
/* Run types: the activity-log category colours. */
.ri-tag--daily, .ri-pill--daily { --tag-rgb: 96, 165, 250; }
.ri-tag--tempo, .ri-pill--tempo { --tag-rgb: 251, 146, 60; }
.ri-tag--race, .ri-pill--race { --tag-rgb: 248, 113, 113; }
.ri-tag--interval { --tag-rgb: 251, 191, 36; }
/* Confidence and status. */
.ri-tag--high, .ri-tag--success, .ri-pill--success { --tag-rgb: var(--ri-green-rgb, 52, 211, 153); }
.ri-tag--medium, .ri-tag--warning, .ri-pill--warning { --tag-rgb: var(--ri-amber-rgb, 251, 191, 36); }
.ri-tag--low, .ri-tag--danger, .ri-pill--danger { --tag-rgb: var(--ri-red-rgb, 248, 113, 113); }
/* Match tiers: Great match / Good match / Not ideal. */
.ri-tag--match-great, .ri-pill--match-great { --tag-rgb: var(--ri-match-great-rgb, 139, 92, 246); }
.ri-tag--match-good, .ri-pill--match-good { --tag-rgb: var(--ri-match-good-rgb, 1, 195, 168); }
.ri-tag--match-low, .ri-pill--match-low { --tag-rgb: var(--ri-match-low-rgb, 251, 191, 36); }
/* A tag whose label may be long (a shoe name, a reason) can truncate. */
.ri-tag--truncate {
  display: inline-block;
  flex-shrink: 1;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
}

/* --------------------------------------------------------------------------
   Form fields: no box per field. The section card holds label, control and
   helper directly; one focus style; 16 px text up to 1024 px (no iOS zoom).
   <div class="ri-field"><label class="ri-field-label" for="x">…</label>
     <input class="ri-input" id="x"><p class="ri-field-help">…</p></div>
   .ri-input styles <input>, <select> and <textarea>.
   -------------------------------------------------------------------------- */
.ri-fields {
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  gap: var(--ri-space-4, 16px);
}
@media (min-width: 768px) {
  .ri-fields--2col {
    grid-template-columns: repeat(2, minmax(0, 1fr));
    column-gap: var(--ri-space-6, 24px);
  }
}
.ri-field {
  display: grid;
  align-content: start;
  gap: var(--ri-space-2, 8px);
  min-width: 0;
  margin: 0;
  padding: 0;
  border: 0;
}
.ri-field-label {
  display: block;
  margin: 0;
  color: rgba(46, 209, 244, 0.7);   /* production's cyan form labels (owner, 2026-10-03) */
  font-size: var(--ri-text-caption, 13px);
  font-weight: var(--ri-weight-regular, 400);
  line-height: var(--ri-leading-caption, 1.35);
  letter-spacing: 0;
  text-transform: none;
}
/* Form help and the field's own error: the secondary role (SPEC v2). */
.ri-field-help,
.ri-field-error {
  margin: calc(-1 * var(--ri-space-1, 4px)) 0 0;
  font-size: var(--ri-text-secondary, 14px);
  line-height: var(--ri-leading-secondary, 1.4);
}
.ri-field-help { color: var(--ri-text-faint, rgba(255, 255, 255, 0.62)); }
.ri-field-error { color: var(--ri-red, #F87171); }
.ri-field-error[hidden] { display: none; }
/* :is() lifts the rule to (0,1,1) so element rules such as
   dashboard-embed.css's input[type="number"] (80 px wide) lose on order. */
:is(input, select, textarea).ri-input {
  --_input-chevron: none;
  box-sizing: border-box;
  display: block;
  width: 100%;
  min-width: 0;
  height: var(--ri-control-md, 40px);
  margin: 0;
  padding: 0 12px;
  border: 1px solid var(--ri-border-control, rgba(255, 255, 255, 0.36));
  border-radius: var(--ri-radius-sm, 8px);
  background-color: var(--ri-surface-control, rgba(255, 255, 255, 0.05));
  background-image: var(--_input-chevron);
  background-repeat: no-repeat;
  background-position: right 12px center;
  background-size: 12px 12px;
  box-shadow: 0 4px 16px rgba(0, 0, 0, 0.1);
  color: var(--ri-text-strong, rgba(255, 255, 255, 0.96));
  font-family: var(--ri-font-sans, 'Noto Sans', ui-sans-serif, system-ui, sans-serif);
  font-size: var(--ri-text-input, 15px);
  font-weight: var(--ri-weight-regular, 400);
  line-height: 20px;
  outline: none;
  appearance: none;
  -webkit-appearance: none;
  color-scheme: dark;
  backdrop-filter: none;
  -webkit-backdrop-filter: none;
  transition-property: border-color, box-shadow;
  transition-duration: var(--ri-dur-fast, 150ms);
  transition-timing-function: var(--ri-ease-standard, cubic-bezier(0.4, 0, 0.2, 1));
}
/* Number fields carry no native stepper (the value is typed; B3 bpm fields). */
.ri-input[type="number"] {
  -moz-appearance: textfield;
  appearance: textfield;
}
.ri-input[type="number"]::-webkit-inner-spin-button,
.ri-input[type="number"]::-webkit-outer-spin-button {
  margin: 0;
  -webkit-appearance: none;
  appearance: none;
}
.ri-input::placeholder {
  color: rgba(255, 255, 255, 0.5);
  opacity: 1;
}
select.ri-input {
  --_input-chevron: url("/images/icons/run-it/chevron-down.svg?v=20261007-icon-edges-v6");
  padding-right: 36px;
  cursor: pointer;
}
select.ri-input option {
  background: var(--ri-surface-overlay, #120F22);
  color: #fff;
}
textarea.ri-input {
  height: auto;
  min-height: 96px;
  padding: 10px 12px;
  line-height: var(--ri-leading-body, 1.5);
  resize: vertical;
}
@media (hover: hover) {
  .ri-input:hover:not(:focus):not(:disabled) { border-color: rgba(255, 255, 255, 0.32); }
}
.ri-input:focus,
.ri-input:focus-visible {
  border-color: var(--ri-cyan, #2ED1F4);
  background-color: var(--ri-focus-field-bg, rgba(46, 209, 244, 0.05));
  box-shadow: var(--ri-focus-field-shadow, inset 0 1px 2px rgba(0, 0, 0, 0.5), 0 0 18px rgba(46, 209, 244, 0.32), 0 0 40px rgba(46, 209, 244, 0.18));
}
.ri-input[aria-invalid="true"] { border-color: var(--ri-red, #F87171); }
.ri-input[aria-invalid="true"]:focus { box-shadow: 0 0 0 3px rgba(248, 113, 113, 0.25); }
.ri-input:disabled {
  opacity: 0.5;
  cursor: not-allowed;
}
@media (max-width: 640px) {
  .ri-input:not(textarea) { height: 44px; }
}

/* Checkbox, radio and switch: native inputs, restyled. Put each in a
   <label class="ri-choice"> row, which is the 44 px tap target. */
.ri-choice {
  display: flex;
  align-items: center;
  gap: var(--ri-space-3, 12px);
  min-height: var(--ri-hit-min, 44px);
  margin: 0;
  color: var(--ri-text-muted, rgba(255, 255, 255, 0.74));
  font-size: var(--ri-text-secondary, 14px);
  line-height: var(--ri-leading-secondary, 1.4);
  cursor: pointer;
}
.ri-choice--split { justify-content: space-between; }
.ri-check,
.ri-radio,
.ri-switch {
  flex: none;
  box-sizing: border-box;
  margin: 0;
  border: 1px solid var(--ri-border-control, rgba(255, 255, 255, 0.36));
  background-color: var(--ri-surface-control, rgba(255, 255, 255, 0.05));
  background-repeat: no-repeat;
  cursor: pointer;
  appearance: none;
  -webkit-appearance: none;
  transition-property: background-color, border-color, background-position;
  transition-duration: var(--ri-dur-fast, 150ms);
  transition-timing-function: var(--ri-ease-standard, cubic-bezier(0.4, 0, 0.2, 1));
}
.ri-check {
  width: 18px;
  height: 18px;
  border-radius: var(--ri-radius-xs, 4px);
  background-position: center;
  background-size: 12px 12px;
}
.ri-check:checked {
  border-color: var(--ri-cyan, #2ED1F4);
  background-color: var(--ri-cyan, #2ED1F4);
  background-image: url("/images/icons/run-it/check.svg?v=20261007-icon-edges-v6");
}
.ri-radio {
  width: 18px;
  height: 18px;
  border-radius: 50%;
}
.ri-radio:checked {
  border-color: var(--ri-cyan, #2ED1F4);
  background-color: var(--ri-cyan, #2ED1F4);
  background-image: radial-gradient(circle, #030014 0 3.5px, transparent 4px);
}
/* role="switch" on the checkbox, drawn as the app's premium toggle (the
   activity log's .rii-toggle, owner 2026-10-04: "re-use our toggle"): a
   52 x 26 track, a metallic silver knob with a soft shadow; on, the track
   turns cyan and the knob slides right. Unlike .rii-toggle the checkbox
   itself is the control, so it stays keyboard-focusable. */
.ri-switch {
  position: relative;
  width: 52px;
  height: 26px;
  border: 0;
  border-radius: 13px;
  background-color: rgba(255, 255, 255, 0.15);
  background-image: none;
}
.ri-switch::after {
  content: "";
  position: absolute;
  top: 0;
  left: 0;
  width: 26px;
  height: 26px;
  border-radius: 50%;
  background: conic-gradient(rgb(160, 160, 160), #fff, rgb(160, 160, 160), #fff, rgb(160, 160, 160));
  box-shadow: 3px 1px 5px rgba(0, 0, 0, 0.3);
  transition: transform var(--ri-dur-fast, 150ms) var(--ri-ease-standard, cubic-bezier(0.4, 0, 0.2, 1));
}
.ri-switch:checked {
  background-color: rgba(46, 209, 244, 0.45);
  background-image: none;
}
.ri-switch:checked::after { transform: translateX(26px); }
.ri-switch:disabled:checked { background-color: rgba(46, 209, 244, 0.25); }
.ri-check:focus-visible,
.ri-radio:focus-visible,
.ri-switch:focus-visible {
  outline: 2px solid var(--ri-cyan, #2ED1F4);
  outline-offset: 2px;
}
.ri-check:disabled,
.ri-radio:disabled,
.ri-switch:disabled {
  opacity: 0.5;
  cursor: not-allowed;
}

/* --------------------------------------------------------------------------
   Notice line: one message in a fixed 42 px line (the provider panels'
   panel-message-line rule: a message never resizes its panel). Keep the
   .ri-notice-slot in the layout; show and hide the notice inside it.
   <div class="ri-notice-slot"><div class="ri-notice ri-notice--success"
     role="status"><svg class="ri-notice-icon" …/><p class="ri-notice-text">
     <strong>Synced.</strong> 3 new runs.</p></div></div>
   Tones: default neutral, --info (amber: asks the runner to act),
   --success (green), --error (red). Copy fits two lines; longer text is
   clamped, so keep the full text in title="".
   -------------------------------------------------------------------------- */
.ri-notice-slot {
  min-height: var(--ri-notice-line, 42px);
}
.ri-notice {
  --_notice-rgb: 255, 255, 255;
  box-sizing: border-box;
  display: grid;
  grid-template-columns: auto minmax(0, 1fr) auto;
  align-items: center;
  gap: var(--ri-space-2, 8px);
  height: var(--ri-notice-line, 42px);
  margin: 0;
  padding: 3px 10px;
  overflow: hidden;
  border: 1px solid rgba(var(--_notice-rgb), 0.14);
  border-radius: var(--ri-radius-sm, 8px);
  background: rgba(var(--_notice-rgb), 0.04);
  color: rgba(255, 255, 255, 0.78);
}
.ri-notice[hidden] { display: none; }

/* Messages fade in and fade out (owner 2026-10-05): the toast timings. A
   notice or field error toggled with [hidden] fades out before it leaves the
   layout and fades in from its starting style; data-leaving lets a page fade
   a notice out first and set [hidden] afterwards. */
:is(.ri-notice, .ri-field-error) {
  opacity: 1;
  transition:
    opacity var(--ri-dur-toast-in, 200ms) var(--ri-ease-out, ease-out),
    display var(--ri-dur-toast-in, 200ms) allow-discrete;
}
:is(.ri-notice, .ri-field-error):is([hidden], [data-leaving]) {
  opacity: 0;
  transition-duration: var(--ri-dur-toast-out, 160ms);
  transition-timing-function: var(--ri-ease-in, ease-in);
}
@starting-style {
  :is(.ri-notice, .ri-field-error) { opacity: 0; }
}
@media (prefers-reduced-motion: reduce) {
  :is(.ri-notice, .ri-field-error) { transition: none; }
}
.ri-notice-icon {
  flex: none;
  width: var(--ri-icon-sm, 16px);
  height: var(--ri-icon-sm, 16px);
  color: rgb(var(--_notice-rgb));
}
.ri-notice-text {
  display: -webkit-box;
  -webkit-box-orient: vertical;
  -webkit-line-clamp: 2;
  overflow: hidden;
  margin: 0;
  /* Caption size with a 1.3 leading: two lines must fit the fixed 42 px
     message line (42 - 2 x 3 px padding - 2 x 1 px border = 34 px; the
     caption leading 1.35 would need 35.1 px). Exception to SPEC rule 3. */
  font-size: var(--ri-text-caption, 13px);
  line-height: 1.3;
}
.ri-notice-text strong {
  color: #fff;
  font-weight: var(--ri-weight-semibold, 600);
}
.ri-notice--info {
  --_notice-rgb: var(--ri-amber-rgb, 251, 191, 36);
  border-color: rgba(var(--_notice-rgb), 0.34);
  background: rgba(var(--_notice-rgb), 0.1);
  color: #FDE68A;
}
.ri-notice--success {
  --_notice-rgb: var(--ri-green-rgb, 52, 211, 153);
  border-color: rgba(var(--_notice-rgb), 0.35);
  background: rgba(var(--_notice-rgb), 0.09);
  color: #A7F3D0;
}
.ri-notice--error {
  --_notice-rgb: var(--ri-red-rgb, 248, 113, 113);
  border-color: rgba(var(--_notice-rgb), 0.38);
  background: rgba(var(--_notice-rgb), 0.1);
  color: #FCA5A5;
}

/* --------------------------------------------------------------------------
   Dialog: the one dialog recipe (audit V2-20, V3-17, S-21) for a page's own
   dialog; the RunItUI dialogs, the Contributor and Pro modals and the
   retailer chooser draw the same recipe from pro-app.css and their scripts.
   <dialog class="ri-dialog" aria-labelledby="t">
     <button type="button" class="ri-btn ri-btn--ghost ri-btn--icon ri-btn--sm ri-btn--close ri-dialog-close" aria-label="Close">…x…</button>
     <h2 class="ri-dialog-title" id="t">Title</h2>
     <p class="ri-dialog-copy">…</p>
     <div class="ri-dialog-actions"><button class="ri-btn ri-btn--ghost">Cancel</button><button class="ri-btn ri-btn--primary">Save</button></div>
   </dialog>
   Native <dialog>: showModal() gives the ink backdrop below. A div needs its
   own fixed backdrop element with .ri-dialog-backdrop. Motion: RunItMotion
   kind 'modal' (240/180 ms) and 'backdrop' (200/160 ms).
   -------------------------------------------------------------------------- */
.ri-dialog {
  position: relative;
  box-sizing: border-box;
  width: min(calc(100vw - 32px), var(--ri-dialog-width, 520px));
  max-height: calc(100dvh - var(--ri-popup-gap-top, 16px) - var(--ri-popup-gap-bottom, 16px));
  overflow: auto;
  margin: auto;
  padding: var(--ri-dialog-pad, 24px);
  border: 1px solid var(--ri-border-dialog, rgba(46, 209, 244, 0.2));
  border-radius: var(--ri-radius-lg, 16px);
  background: var(--ri-surface-dialog);
  box-shadow: var(--ri-shadow-overlay, 0 16px 48px rgba(0, 0, 0, 0.5));
  color: #fff;
  font-family: var(--ri-font-sans, 'Noto Sans', ui-sans-serif, system-ui, sans-serif);
  text-align: left;
  overscroll-behavior: contain;
}
/* A modal <dialog> sits in the top layer: keep it fixed and centred (the
   relative position above only serves a dialog drawn inside a backdrop div). */
dialog.ri-dialog:modal {
  position: fixed;
  inset: var(--ri-popup-gap-top, 16px) 0 var(--ri-popup-gap-bottom, 16px);
  height: fit-content;
}
.ri-dialog::backdrop,
.ri-dialog-backdrop {
  background: var(--ri-backdrop, rgba(3, 0, 20, 0.6));
  backdrop-filter: blur(var(--ri-backdrop-blur, 12px));
  -webkit-backdrop-filter: blur(var(--ri-backdrop-blur, 12px));
}
.ri-dialog-backdrop {
  position: fixed;
  inset: 0;
  display: grid;
  place-items: center;
  padding: var(--ri-popup-gap-top, 16px) 16px var(--ri-popup-gap-bottom, 16px);
}
.ri-dialog-title {
  margin: 0 0 8px;
  padding: 0 40px 0 0;
  color: #fff;
  font-size: var(--ri-text-title, 24px);
  font-weight: var(--ri-weight-light, 300);
  line-height: var(--ri-leading-title, 1.25);
  letter-spacing: var(--ri-tracking-title, -0.01em);
  text-align: left;
  text-shadow: none;
}
.ri-dialog-title::before,
.ri-dialog-title::after { content: none; }
.ri-dialog-copy {
  margin: 0 0 20px;
  color: var(--ri-text-muted, rgba(255, 255, 255, 0.74));
  font-size: var(--ri-text-body, 15px);
  line-height: var(--ri-leading-body, 1.5);
}
.ri-dialog-close {
  position: absolute;
  top: 12px;
  right: 12px;
}
.ri-dialog-actions {
  display: flex;
  flex-wrap: wrap;
  justify-content: flex-end;
  gap: var(--ri-space-2, 8px);
  margin-top: 20px;
}
@media (max-width: 640px) {
  .ri-dialog-actions { flex-direction: column-reverse; }
  .ri-dialog-actions > * { width: 100%; }
}

/* --------------------------------------------------------------------------
   ri-lock: the legacy global !important rules these components must survive
   until they are deleted. Each group names the rule it defeats.
   -------------------------------------------------------------------------- */
@layer ri-lock {
  /* A lime button's focus ring is lime: a cyan ring round the lime fill read
     as a stray border (owner 2026-10-12, Pro page "weird button styling"). In
     the lock so it outranks pro-app.css's page-wide cyan !important ring. */
  .ri-btn--lime:focus-visible { outline-color: var(--ri-lime, #B7FF00) !important; }
  /* The copied clamp() type block in seven page stylesheets (audit S-01:
     h1-h6, p, label, button, input/select/textarea sizes and phone line
     heights) and dashboard-embed.css's .app-page-* phone rules. */
  .ri-page-head {
    margin: 0 0 var(--ri-page-head-gap, 48px) !important;
    padding: 0 !important;
  }
  .ri-page-title {
    margin: 0 !important;
    padding: 0 0 var(--ri-title-underline-gap, 12px) !important;
    font-size: var(--ri-text-display, 40px) !important;
    line-height: var(--ri-leading-display, 1.1) !important;
    letter-spacing: var(--ri-tracking-display, -0.005em) !important;
  }
  .ri-page-lead {
    margin: var(--ri-title-lead-gap, 12px) 0 0 !important;
    font-size: var(--ri-text-lead, 16px) !important;
    line-height: var(--ri-leading-lead, 1.4) !important;
  }
  .ri-section-title {
    font-size: var(--ri-text-title, 24px) !important;
    line-height: var(--ri-leading-title, 1.25) !important;
  }
  .ri-card-title {
    font-size: var(--ri-text-subtitle, 18px) !important;
    line-height: var(--ri-leading-subtitle, 1.3) !important;
  }
  .ri-btn {
    min-height: 0 !important;
    padding: 0 var(--_btn-px) !important;
    font-size: var(--_btn-fs) !important;
    line-height: var(--_btn-lh) !important;
  }
  .ri-eyebrow {
    font-size: var(--ri-text-caption, 13px) !important;
    line-height: var(--ri-leading-caption, 1.35) !important;
  }
  .ri-field-label {
    font-size: var(--ri-text-caption, 13px) !important;
    line-height: var(--ri-leading-caption, 1.35) !important;
  }
  .ri-field-help,
  .ri-field-error {
    font-size: var(--ri-text-secondary, 14px) !important;
    line-height: var(--ri-leading-secondary, 1.4) !important;
  }
  .ri-input {
    font-size: var(--ri-text-input, 15px) !important;
  }
  .ri-notice-text {
    font-size: var(--ri-text-caption, 13px) !important;
    line-height: 1.3 !important;
  }

  /* Page-level global field :focus rules with !important (cyan wash, 12 px
     blur, 15-40 px glows: garmin-profile.html, upload.html, support.html,
     profile-page.css, stryd-profile-page.css). One focus style for every
     field instead. pro-app.css's copy of that rule was deleted on
     2026-09-27; this group goes once the page copies are gone. */
  .ri-input:focus,
  .ri-input:focus-visible {
    outline: none !important;
    outline-offset: 0 !important;
    border-color: var(--ri-cyan, #2ED1F4) !important;
    background-color: var(--ri-focus-field-bg, rgba(46, 209, 244, 0.05)) !important;
    background-image: var(--_input-chevron) !important;
    background-repeat: no-repeat !important;
    background-position: right 12px center !important;
    background-size: 12px 12px !important;
    box-shadow: var(--ri-focus-field-shadow, inset 0 1px 2px rgba(0, 0, 0, 0.5), 0 0 18px rgba(46, 209, 244, 0.32), 0 0 40px rgba(46, 209, 244, 0.18)) !important;
    backdrop-filter: none !important;
    -webkit-backdrop-filter: none !important;
  }
  .ri-input[aria-invalid="true"]:focus {
    border-color: var(--ri-red, #F87171) !important;
    box-shadow: 0 0 0 3px rgba(248, 113, 113, 0.25) !important;
  }

  /* Dropdown menus: one look for every list that opens under a trigger
     (owner 2026-10-06: "take B, use this for ALL dropdowns"). The menus:
     the shoe search and brand/model suggestions (.shoe-dropdown; Shoe
     Analysis, Profile), the Shoe Analysis catalogue menus and Sort
     (.catalogue-select-menu) and Brand checklist, the Garmin/Stryd/Strava/
     Apple source switcher, the Drift Inspector run picker, the Reset
     windows menu and Upload's suggestions. Each page restyled its own, many
     with !important, so the one look lives here in ri-lock:
     - the popover surface (fully opaque, no blur), a cyan edge, radius 12,
       the overlay shadow with a soft cyan halo;
     - rows flush on cyan hairlines, 44 px tall;
     - hover and the keyboard highlight: a full-row cyan wash, no side bar;
     - the selected row: cyan text and a cyan check.
     "Add new" and custom suggestion rows keep their own tint and hover. */
  :is(.shoe-dropdown, .catalogue-select-menu, .catalogue-brand-filter [data-filter-brands], .rii-data-toggle-menu, .di-dropdown-menu, .dw-reset-menu, .upload-suggestions) {
    padding: 0 !important;
    border: 1px solid var(--ri-border-menu, rgba(46, 209, 244, 0.28)) !important;
    border-radius: var(--ri-radius-md, 12px) !important;
    background: var(--ri-surface-menu, var(--ri-surface-overlay, #120F22)) !important;
    box-shadow: var(--ri-shadow-menu, 0 16px 48px rgba(0, 0, 0, 0.5), 0 0 24px rgba(46, 209, 244, 0.12)) !important;
    -webkit-backdrop-filter: none !important;
    backdrop-filter: none !important;
  }
  /* The two menus that don't scroll still clip their rows to the corners. */
  :is(.rii-data-toggle-menu, .dw-reset-menu) { overflow: hidden !important; }

  /* Rows. :where() keeps these at zero specificity, so hover, the keyboard
     highlight and the selected state below always win. The source switcher
     keeps its logo row height; the text rows share one size. */
  :where(.shoe-option, .catalogue-select-menu [role="option"], .catalogue-brand-filter [data-filter-brands] label, .rii-data-toggle-menu .rii-data-toggle-item, .di-dropdown-item, .dw-reset-item, .upload-suggestion) {
    margin: 0 !important;
    border: 0 !important;
    border-bottom: 1px solid var(--ri-menu-divider, rgba(46, 209, 244, 0.08)) !important;
    border-radius: 0 !important;
    box-shadow: none !important;
  }
  :where(.shoe-option, .catalogue-select-menu [role="option"], .catalogue-brand-filter [data-filter-brands] label, .rii-data-toggle-menu .rii-data-toggle-item, .di-dropdown-item, .dw-reset-item, .upload-suggestion):last-child {
    border-bottom: 0 !important;
  }
  :where(.shoe-option, .catalogue-select-menu [role="option"], .catalogue-brand-filter [data-filter-brands] label, .di-dropdown-item, .dw-reset-item, .upload-suggestion) {
    min-height: 44px !important;
    padding: 12px 16px !important;
    font-size: var(--ri-text-secondary, 14px) !important;
  }
  :where(.shoe-option, .catalogue-select-menu [role="option"], .catalogue-brand-filter [data-filter-brands] label, .rii-data-toggle-menu .rii-data-toggle-item, .di-dropdown-item, .dw-reset-item, .upload-suggestion):not(.add-new):not([data-custom="true"]) {
    background: transparent !important;
  }
  .dw-reset-item { display: flex !important; align-items: center; }

  /* Hover (pointer) and the keyboard highlight: the cyan wash. */
  @media (hover: hover) {
    :where(.shoe-option, .catalogue-select-menu [role="option"], .catalogue-brand-filter [data-filter-brands] label, .rii-data-toggle-menu .rii-data-toggle-item, .di-dropdown-item, .dw-reset-item, .upload-suggestion):not(.add-new):not([data-custom="true"]):not(:disabled):not([aria-disabled="true"]):hover {
      background: var(--ri-menu-hover, rgba(46, 209, 244, 0.12)) !important;
    }
  }
  .catalogue-select-menu .is-highlighted:not([aria-disabled="true"]) {
    background: var(--ri-menu-hover, rgba(46, 209, 244, 0.12)) !important;
    padding-left: 16px !important;
    border-left: 0 !important;
  }

  /* Selected: the check (the cyan text is outside this layer, below). */
  :is(.catalogue-select-menu [aria-selected="true"], .di-dropdown-item.is-selected)::after {
    content: "" !important;
    flex: none;
    width: 16px;
    height: 16px;
    background-color: var(--ri-cyan, #2ED1F4);
    -webkit-mask: var(--ri-menu-check) center / contain no-repeat;
    mask: var(--ri-menu-check) center / contain no-repeat;
  }
  /* The source switcher rows are logos: the check sits at the right edge
     (its ::after is the plate behind the logo, so the check takes ::before,
     which used to draw a bar on the left). */
  .rii-data-toggle-menu .rii-data-toggle-item.is-active::before {
    content: "" !important;
    position: absolute !important;
    top: 50% !important;
    right: 12px !important;
    bottom: auto !important;
    left: auto !important;
    z-index: 2 !important;
    width: 16px !important;
    height: 16px !important;
    border-radius: 0 !important;
    transform: translateY(-50%);
    background: rgb(var(--_source-rgb, 46, 209, 244)) !important;
    -webkit-mask: var(--ri-menu-check) center / contain no-repeat;
    mask: var(--ri-menu-check) center / contain no-repeat;
  }
  /* Each source row lights in its own colour (owner 2026-10-06:
     "conditionally color on hover"), the colours the closed switcher
     already wears: Garmin cyan, Stryd yellow, Strava orange, Apple green.
     The check on the chosen row takes the same colour. */
  .rii-data-toggle-menu .rii-data-toggle-item { --_source-rgb: var(--ri-cyan-rgb, 46, 209, 244); }
  .rii-data-toggle-menu .rii-data-toggle-item:is([data-logo-alt="Stryd"], [data-source="stryd"]) { --_source-rgb: 254, 202, 47; }
  .rii-data-toggle-menu .rii-data-toggle-item:is([data-logo-alt="Strava"], [data-source="strava"]) { --_source-rgb: 252, 76, 2; }
  .rii-data-toggle-menu .rii-data-toggle-item:is([data-logo-alt="Apple"], [data-source="apple"]) { --_source-rgb: 32, 199, 122; }
  @media (hover: hover) {
    .rii-data-toggle .rii-data-toggle-menu .rii-data-toggle-item:is([data-logo-alt], [data-source]):not(:disabled):hover {
      background: rgba(var(--_source-rgb, 46, 209, 244), 0.12) !important;
    }
  }
  .rii-data-toggle .rii-data-toggle-menu .rii-data-toggle-item:is([data-logo-alt], [data-source]):focus-visible {
    background: rgba(var(--_source-rgb, 46, 209, 244), 0.12) !important;
  }
  /* The logos sit straight on the menu surface: the black plate behind
     each one was drawn for the old black list. */
  .rii-data-toggle-menu .rii-data-toggle-item::after { content: none !important; }
  .rii-data-toggle-menu .rii-data-toggle-item .rii-data-toggle-logo {
    background: transparent !important;
    box-shadow: none !important;
  }
  /* The run picker's bottom fade melts into the new surface. */
  .di-dropdown-fade {
    background: linear-gradient(to top, var(--ri-surface-overlay, #120F22) 0%, rgba(18, 15, 34, 0) 100%) !important;
    border-radius: 0 0 var(--ri-radius-md, 12px) var(--ri-radius-md, 12px) !important;
  }

  /* Native <select> lists take the same look where the browser lets CSS draw
     them (appearance: base-select; Chrome 135+), on mouse and trackpad only:
     phones keep their own picker (iOS wheel, Android sheet). The closed field
     keeps its own look; only the list and the browser's own arrow change.
     In ri-lock because pages force appearance: none and paint <option>s
     with !important. */
  @supports (appearance: base-select) {
    @media (hover: hover) and (pointer: fine) {
      select:not([multiple]):not([size]),
      select:not([multiple]):not([size])::picker(select) {
        appearance: base-select !important;
      }
      select:not([multiple]):not([size])::picker-icon { display: none !important; }
      /* The closed field as before: its value centred, on one line (a
         base-select field otherwise top-aligns and wraps its value). */
      select:not([multiple]):not([size]) {
        align-items: center !important;
        align-content: center !important;
        white-space: pre !important;
      }
      select:not([multiple]):not([size])::picker(select) {
        margin: 4px 0 !important;
        padding: 0 !important;
        border: 1px solid var(--ri-border-menu, rgba(46, 209, 244, 0.28)) !important;
        border-radius: var(--ri-radius-md, 12px) !important;
        background: var(--ri-surface-menu, var(--ri-surface-overlay, #120F22)) !important;
        box-shadow: var(--ri-shadow-menu, 0 16px 48px rgba(0, 0, 0, 0.5), 0 0 24px rgba(46, 209, 244, 0.12)) !important;
        font-family: var(--ri-font-sans, 'Noto Sans', ui-sans-serif, system-ui, sans-serif) !important;
        font-size: var(--ri-text-secondary, 14px) !important;
        line-height: 1.4 !important;
        scrollbar-width: thin;
      }
      select:not([multiple]):not([size]) option {
        display: flex !important;
        align-items: center !important;
        gap: 12px !important;
        min-height: 44px !important;
        padding: 12px 16px !important;
        border: 0 !important;
        border-bottom: 1px solid var(--ri-menu-divider, rgba(46, 209, 244, 0.08)) !important;
        border-radius: 0 !important;
        background: transparent !important;
        outline: none !important;
        font-weight: var(--ri-weight-regular, 400) !important;
      }
      select:not([multiple]):not([size]) option:last-child { border-bottom: 0 !important; }
      select:not([multiple]):not([size]) option:not(:disabled):is(:hover, :focus-visible) {
        background: var(--ri-menu-hover, rgba(46, 209, 244, 0.12)) !important;
      }
      /* The check sits at the right, as in the custom menus. */
      select:not([multiple]):not([size]) option::checkmark {
        order: 1 !important;
        margin-inline-start: auto !important;
        content: "" !important;
        width: 16px !important;
        height: 16px !important;
        background-color: var(--ri-cyan, #2ED1F4) !important;
        -webkit-mask: var(--ri-menu-check) center / contain no-repeat;
        mask: var(--ri-menu-check) center / contain no-repeat;
      }
    }
  }

  /* Lives in the lock: page CSS draws these section surfaces with !important. */
  /* Phones: no outer cards (owner 2026-10-06: "drop ALL the cards … only icon
     + title"). Every top-level card in <main> drops its surface (fill,
     border, glow, corners) and becomes part of the page: its icon + title,
     copy and content sit on the page's 16 px column, under the page title, and
     its header loses the card hairline. Cards inside a section (tiles, rows,
     panels) keep their look, so there is never a card in a card. Dialogs keep
     their card; desktop keeps every card. Exception: .ri-card--keep-phone, a
     card that is the content itself, like a popup's (the Pro page's plan cards
     and the Welcome to Pro hero; owner 2026-10-12). */
  @media (max-width: 640px) {
    main .ri-card:not(.ri-card .ri-card):not(.ri-dialog .ri-card):not([role="dialog"] .ri-card):not(.ri-card--keep-phone) {
      padding: 0 calc(0.5rem - 1px) !important;
      border-color: transparent !important;
      border-radius: 0 !important;
      background: none !important;
      box-shadow: none !important;
    }
    main .ri-card:not(.ri-card .ri-card):not(.ri-dialog .ri-card):not([role="dialog"] .ri-card):not(.ri-card--keep-phone)::before,
    main .ri-card:not(.ri-card .ri-card):not(.ri-dialog .ri-card):not([role="dialog"] .ri-card):not(.ri-card--keep-phone)::after {
      content: none !important;
    }
    /* Page headings keep 20 px before their content, so titles and the first
       field label or tile read separately. The same gap applies to every
       section, including Dashboard's animated card stages. */
    main .ri-card:not(.ri-card .ri-card):not(.ri-dialog .ri-card):not([role="dialog"] .ri-card):not(.ri-card--keep-phone) > .ri-card-head,
    main .ri-card:not(.ri-card .ri-card):not(.ri-dialog .ri-card):not([role="dialog"] .ri-card):not(.ri-card--keep-phone) > .dash-card-stage > .ri-card-head,
    main .ri-card:not(.ri-card .ri-card):not(.ri-dialog .ri-card):not([role="dialog"] .ri-card):not(.ri-card--keep-phone) > .dash-card-stage > .dash-card-skeleton > .ri-card-head {
      gap: 6px !important;
      margin-bottom: var(--ri-space-5, 20px) !important;
      padding-bottom: 0 !important;
      background: none !important;
    }
    /* Charts as wide as the phone allows (owner 2026-10-06). A section chart
       leaves the 16 px column for 4 px from each screen edge, as the Fatigue
       Resilience chart already did; the Biomechanical Signature's axis
       header, chart and pace slider widen together, so each slider bar stays
       under its point. A chart inside a tile reaches 4 px from the tile edge.
       The run page's "Your run" chart too (owner 2026-10-12: "can be wider"). */
    :is(.dash-bio-chart-stage,
        #biomechanicalProfileSection :is(#bioAxisHeader, .drift-chart-container, .bio-bracket-slider-wrap),
        .shoes-chart-wrap, #paceChartContainer, #runChartBox) {
      /* An explicit width: some chart boxes pair a fixed height with an
         aspect-ratio, which turns an auto width into height x ratio. */
      width: calc(100vw - 8px) !important;
      max-width: none !important;
      margin-left: calc(50% - 50vw + 4px) !important;
      margin-right: calc(50% - 50vw + 4px) !important;
    }
    /* The signature's pace labels are centred on its outer points: 8 px keeps
       "5:20" and "3:40" off the screen edge. */
    #biomechanicalProfileSection :is(#bioAxisHeader, .drift-chart-container, .bio-bracket-slider-wrap) {
      width: calc(100vw - 16px) !important;
      margin-left: calc(50% - 50vw + 8px) !important;
      margin-right: calc(50% - 50vw + 8px) !important;
    }
    :is(.legacy-report-chart-stage, .sp-trend-card .sp-trend-wrap, #stravaSyncSection .strava-trend-chart-wrap) {
      width: auto !important;
      max-width: none !important;
      margin-left: -8px !important;
      margin-right: -8px !important;
    }
  }
}


/* Dropdown menu text (owner 2026-10-06: "make the selected text WHITE and
   the other text GREY"): every row grey, the chosen row white (its check
   stays cyan), and the row under the pointer or the keyboard highlight
   white too. Kept out of ri-lock, which holds no colour. The rows' own page
   colours are single classes; the Brand checklist has its own rule because
   the catalogue's grey filter-label colour needs the id to beat it. */
:is(.shoe-option, .catalogue-select-menu [role="option"], .di-dropdown-item, .dw-reset-item:not(:disabled), .upload-suggestion):not([aria-disabled="true"]) {
  color: var(--ri-text-faint, rgba(255, 255, 255, 0.62));
}
@media (hover: hover) {
  :is(.shoe-option, .catalogue-select-menu [role="option"], .di-dropdown-item, .dw-reset-item:not(:disabled), .upload-suggestion):not([aria-disabled="true"]):hover {
    color: var(--ri-text-strong, rgba(255, 255, 255, 0.96));
  }
}
.catalogue-select-menu [role="option"].is-highlighted:not([aria-disabled="true"]),
:is(.shoe-option[aria-selected="true"], .catalogue-select-menu [role="option"][aria-selected="true"], .di-dropdown-item.is-selected) {
  color: var(--ri-text-strong, rgba(255, 255, 255, 0.96));
}
#shoeCatalogueFilters .catalogue-brand-filter [data-filter-brands] label {
  color: var(--ri-text-faint, rgba(255, 255, 255, 0.62));
}
@media (hover: hover) {
  #shoeCatalogueFilters .catalogue-brand-filter [data-filter-brands] label:hover {
    color: var(--ri-text-strong, rgba(255, 255, 255, 0.96));
  }
}
#shoeCatalogueFilters .catalogue-brand-filter [data-filter-brands] label:has(input:checked) {
  color: var(--ri-text-strong, rgba(255, 255, 255, 0.96));
}
@supports (appearance: base-select) {
  @media (hover: hover) and (pointer: fine) {
    html select:not([multiple]):not([size]) option { color: var(--ri-text-faint, rgba(255, 255, 255, 0.62)); }
    html select:not([multiple]):not([size]) option:is(:hover, :focus-visible, :checked) { color: var(--ri-text-strong, rgba(255, 255, 255, 0.96)); }
    html select:not([multiple]):not([size]) option:disabled { color: rgba(255, 255, 255, 0.38); }
  }
}
/* --------------------------------------------------------------------------
   Filter card: the popup look (owner 2026-10-04: "the filter card should be
   like the card on the top right", the Profile completion dropdown): the
   dialog surface (a cyan glow at the top, fainter at the bottom), the cyan
   dialog edge, radius 16 and the overlay shadow. One rule for every filter
   panel inside a section: the Runs pages' Biomechanical Signature and
   Fatigue Resilience filters, Shoe Analysis' run-focus filters and its
   catalogue Model / Size / More filters panels, My Shoes' chart controls, and
   the phone rows that fold them. The ids outrank the page copies of the old
   tile.
   -------------------------------------------------------------------------- */
:is(#bioControlsGrid, #frpControlsGrid, #analysisFilterGrid, #shoeCatalogueFilters .catalogue-rail, #shoeCatalogueFilters .catalogue-extra-filters, #shoesDetailCard .shoes-chart-toolbar, .ri-filter-bar, #analysisFilterToggle) {
  border: 1px solid var(--ri-border-dialog, rgba(46, 209, 244, 0.2));
  border-radius: var(--ri-radius-lg, 16px);
  background: var(--ri-surface-dialog, var(--ri-surface-overlay, #120F22));
  box-shadow: var(--ri-shadow-overlay, 0 16px 48px rgba(0, 0, 0, 0.5));
}
:is(#bioControlsGrid, #frpControlsGrid).bio-compare-open {
  border-bottom-right-radius: 0;
  border-bottom-left-radius: 0;
}

/* --------------------------------------------------------------------------
   Phone filter bar and sheet (owner 2026-10-06, option D: "implement this
   for mobile filters"). On phones every page's filters are one bar in the
   filter card: Filters, with the number of active filters, and the page's
   one extra action (Sort on Shoe Analysis, Compare on the Runs pages); under
   a hairline, the result count and what is shown. Filters opens one bottom
   sheet with the page's real controls inside (js/ri-filter-sheet.js); every
   choice in it is a dropdown or field, as on desktop.
   -------------------------------------------------------------------------- */
.ri-filter-bar {
  box-sizing: border-box;
  padding: 8px;
}
.ri-filter-bar-actions {
  display: flex;
  gap: 6px;
}
.ri-filter-bar-actions > .ri-btn {
  flex: 1 1 auto;
  min-width: 0;
}
.ri-filter-bar-icon {
  flex: none;
  width: var(--ri-icon-sm, 16px);
  height: var(--ri-icon-sm, 16px);
}
/* The number of active filters, beside "Filters". */
.ri-filter-count {
  display: inline-grid;
  place-items: center;
  box-sizing: border-box;
  min-width: 18px;
  height: 18px;
  padding: 0 5px;
  border-radius: var(--ri-radius-pill, 999px);
  background: var(--ri-cyan, #2ED1F4);
  color: var(--ri-bg, #030014);
  font-size: var(--ri-text-pill, 12px);
  font-weight: var(--ri-weight-medium, 500);
  line-height: 1;
}
.ri-filter-count[hidden] { display: none; }
.ri-filter-bar-status {
  display: flex;
  align-items: baseline;
  gap: 8px;
  min-width: 0;
  margin: 8px 0 0;
  padding: 8px 4px 0;
  border-top: 1px solid rgba(46, 209, 244, 0.1);
  color: var(--ri-text-faint, rgba(255, 255, 255, 0.62));
  font-size: var(--ri-text-caption, 13px);
  font-weight: var(--ri-weight-light, 300);
  line-height: var(--ri-leading-caption, 1.35);
  white-space: nowrap;
}
.ri-filter-bar-status > strong {
  flex: none;
  color: var(--ri-text-strong, rgba(255, 255, 255, 0.96));
  font-weight: var(--ri-weight-regular, 400);
}
.ri-filter-bar-status > span {
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
}
/* A comparison that is on: its own line in cyan under the shown runs. */
.ri-filter-bar-status--compare {
  margin-top: 0;
  padding-top: 4px;
  border-top: 0;
  color: rgba(46, 209, 244, 0.85);
}
.ri-filter-bar-status--compare > strong { color: var(--ri-cyan, #2ED1F4); }
.ri-filter-bar-status[hidden] { display: none; }

/* The sheet: the dialog surface, docked to the bottom edge, its own body
   scrolling between a fixed head and footer. */
dialog.ri-filter-sheet:modal {
  display: flex;
  flex-direction: column;
  inset: auto 0 0;
  width: 100%;
  max-width: none;
  height: auto;
  max-height: min(88dvh, calc(100dvh - 40px));
  margin: 0;
  padding: 0;
  overflow: hidden;
  border-width: 1px 0 0;
  border-radius: var(--ri-radius-lg, 16px) var(--ri-radius-lg, 16px) 0 0;
  box-shadow: 0 -16px 48px rgba(0, 0, 0, 0.55);
}
.ri-filter-sheet-head {
  position: relative;
  display: flex;
  flex: none;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  padding: 22px 16px 12px;
  touch-action: none;
}
.ri-filter-sheet-grab {
  position: absolute;
  top: 8px;
  left: 50%;
  width: 36px;
  height: 4px;
  margin-left: -18px;
  border-radius: var(--ri-radius-pill, 999px);
  background: rgba(255, 255, 255, 0.22);
}
.ri-filter-sheet-title { padding: 0; }
.ri-filter-sheet-close { flex: none; }
.ri-filter-sheet-body {
  flex: 1 1 auto;
  min-height: 0;
  overflow-y: auto;
  overscroll-behavior: contain;
  padding: 4px 16px 16px;
}
/* Groups inside the sheet (Model, Size, More filters) sit on cyan hairlines. */
.ri-filter-sheet-group {
  padding: 16px 0;
  border-top: 1px solid rgba(46, 209, 244, 0.1);
}
.ri-filter-sheet-group:first-child {
  padding-top: 4px;
  border-top: 0;
}
.ri-filter-sheet-foot {
  display: flex;
  flex: none;
  gap: 8px;
  padding: 12px 16px calc(12px + env(safe-area-inset-bottom, 0px));
  border-top: 1px solid rgba(46, 209, 244, 0.1);
}
.ri-filter-sheet-foot > .ri-btn { --_btn-h: 44px; }
.ri-filter-sheet-done { flex: 1 1 auto; }

/* --------------------------------------------------------------------------
   Filter fields: production's hover (owner 2026-10-04: "restore this 'on
   hover' animation for ALL the filter fields"). Hovering a filter field
   lifts it 1 px, washes it white, glows it from inside and lightens its
   edge, in white, not cyan (owner 2026-10-04); focus keeps the cyan
   focus-field glow. One rule for every filter row:
   the Runs pages' period/shoe shells (Biomechanical Signature, compare,
   Fatigue Resilience), Shoe Analysis' run-focus filters, My Shoes' chart
   selects, and the Drift Inspector run and metric pickers. Pointer
   devices only; no blur (only dialogs blur); no lift with reduced motion.
   The :is() list carries a two-class weight so it outranks the page copies
   of these fields without !important.
   -------------------------------------------------------------------------- */
:is(.bio-date-shell, .bio-select-shell:not(:has(> .ri-input)), .bio-select-shell > select.ri-input, .shoes-chart-select, .di-dropdown-trigger, .di-hero-label .di-visual-metric-shell) {
  transition-property: border-color, background-color, box-shadow, transform;
  transition-duration: 0.22s, 0.22s, 0.22s, 0.18s;
  transition-timing-function: ease;
}
:is(.bio-date-shell, .bio-select-shell:not(:has(> .ri-input))):focus-within {
  border-color: var(--ri-cyan, #2ED1F4);
  background-color: var(--ri-focus-field-bg, rgba(46, 209, 244, 0.05));
  box-shadow: var(--ri-focus-field-shadow, inset 0 1px 2px rgba(0, 0, 0, 0.5), 0 0 18px rgba(46, 209, 244, 0.32), 0 0 40px rgba(46, 209, 244, 0.18));
}
@media (hover: hover) {
  :is(.bio-date-shell, .bio-select-shell:not(:has(> .ri-input)), .bio-select-shell > select.ri-input, .shoes-chart-select, .di-dropdown-trigger, .di-hero-label .di-visual-metric-shell):hover:not(:focus-within):not(:focus-visible):not(:disabled):not(.is-open *):not([data-profile-section-state="loading"] *):not(.is-pro-locked *) {
    border-color: rgba(255, 255, 255, 0.42);
    background-color: rgba(255, 255, 255, 0.08);
    box-shadow: inset 0 -5px 12px rgba(255, 255, 255, 0.12), inset 0 5px 12px rgba(255, 255, 255, 0.07);
  }
}
@media (hover: hover) and (prefers-reduced-motion: no-preference) {
  :is(.bio-date-shell, .bio-select-shell:not(:has(> .ri-input)), .bio-select-shell > select.ri-input, .shoes-chart-select, .di-dropdown-trigger, .di-hero-label .di-visual-metric-shell):hover:not(:focus-within):not(:focus-visible):not(:disabled):not(.is-open *):not([data-profile-section-state="loading"] *):not(.is-pro-locked *) {
    transform: translateY(-1px);
  }
}
@media (prefers-reduced-motion: reduce) {
  :is(.bio-date-shell, .bio-select-shell:not(:has(> .ri-input)), .bio-select-shell > select.ri-input, .shoes-chart-select, .di-dropdown-trigger, .di-hero-label .di-visual-metric-shell) {
    transition-property: border-color, background-color, box-shadow;
  }
}

/* ==========================================================================
   Level line (owner 2026-10-07): the one look for every value and progress bar
   in the app, the same light as the Shoes score arc and the Runs bars.
   A glass track; the fill is a faint haze in the bar's colour that brightens
   toward the value, and one bright glowing line sits at the value.
   - Track: .ri-level-track (overflow stays visible so the line can stand
     4 px above and below a thin track).
   - Fill: .ri-level-fill with the bar's colour as --bar-rgb ("r, g, b"); the
     renderer keeps setting the width (or height) and its motion.
   - Vertical bars: .ri-level-fill--vertical (the line sits on top).
   - No value: .is-empty (or width/height 0) hides the line.
   ========================================================================== */
.ri-level-track {
  background: rgba(255, 255, 255, 0.07);
  overflow: visible;
}
.ri-level-fill {
  --bar-rgb: 46, 209, 244;
  position: relative;
  background: linear-gradient(90deg, rgba(var(--bar-rgb), 0.02), rgba(var(--bar-rgb), 0.28));
  box-shadow: none;
}
.ri-level-fill::after {
  content: '';
  position: absolute;
  top: -4px;
  right: -1px;
  bottom: -4px;
  width: 3px;
  border-radius: 2px;
  background: rgb(var(--bar-rgb));
  box-shadow: 0 0 8px rgba(var(--bar-rgb), 0.9), 0 0 2px rgba(var(--bar-rgb), 0.9);
  pointer-events: none;
}
.ri-level-fill--vertical {
  background: linear-gradient(to top, rgba(var(--bar-rgb), 0.02), rgba(var(--bar-rgb), 0.24));
}
.ri-level-fill--vertical::after {
  top: 0;
  right: 0;
  bottom: auto;
  left: 0;
  width: auto;
  height: 3px;
}
.ri-level-fill.is-empty::after,
.ri-level-fill[style*="width:0%"]::after,
.ri-level-fill[style*="width: 0%"]::after,
.ri-level-fill--vertical[style*="height:0%"]::after,
.ri-level-fill--vertical[style*="height: 0%"]::after { display: none; }

/* ==========================================================================
   Score arc (owner 2026-10-07, scoreArcHtml in js/biomech-form-efficiency.js): 240 degrees, open at the
   bottom, the score inside. A faint haze in --arc-color runs up to the score
   and brightens toward it, on a glass track. Same look as the Shoes page arc.
   ========================================================================== */
.ri-score-arc {
  --arc-color: #2ED1F4;
  position: relative;
  display: grid;
  place-items: center;
  width: 112px;
  height: 85px;
  margin: 4px auto 0;
}
.ri-score-arc svg {
  position: absolute;
  inset: 0;
  display: block;
  width: 100%;
  height: 100%;
  overflow: visible;
}
.ri-score-arc path { fill: none; stroke-linecap: round; stroke-width: 5.5; }
.ri-score-arc-track { stroke: rgba(255, 255, 255, 0.07); }
.ri-score-arc-haze-start { stop-color: var(--arc-color); stop-opacity: 0.04; }
.ri-score-arc-haze-end { stop-color: var(--arc-color); stop-opacity: 0.34; }
.ri-score-arc-value {
  position: absolute;
  top: 57%;
  right: 0;
  left: 0;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 2px;
  line-height: 1;
  transform: translateY(-50%);
}
@media (max-width: 640px) {
  .ri-score-arc { width: 84px; height: 64px; margin-top: 2px; }
  .ri-score-arc path { stroke-width: 6.5; }
}

/* Our blueprint shoe drawings (js/shoe-drawings.js) in every spot a shoe photo is missing
   (owner 2026-10-20): the drawing fills its box, faint, so it reads as a placeholder. */
.ri-drawn-shoe {
  display: block;
  flex: 1 1 auto;
  align-self: stretch;
  width: 100%;
  height: 100%;
  min-height: 24px;
  background: center / contain no-repeat;
  opacity: 0.6;
  pointer-events: none;
}
/* The slot that held the question mark takes its photo's whole box for the drawing. */
:is(.rot-photo-empty, .rec-tile-placeholder, .strava-shoe-placeholder, .shoe-opinion-placeholder,
  .owned-shoe-media-placeholder, .dash-shoe-image-placeholder, .dash-rec-img-placeholder):has(> .ri-drawn-shoe) {
  width: 100%;
  height: 100%;
  font-size: 0;
}
