:root {
  --bg: #f5f6f8;
  --card-bg: #ffffff;
  --text: #101114;
  /* 3.3:1 on white -- fails WCAG AA (4.5:1) for the sub-16px text that
     uses this everywhere (captions, row labels). Darkened to 5.15:1,
     same gray family. Dark theme's --text-secondary already passes
     (~5.75:1) and is untouched. */
  --text-secondary: #6b6d78;
  --border: #edeff2;
  /* The mascot's own two greys (see static/mascot.js). It can't just use
     --text: on the dark theme that's #f5f5f7 and a near-white blob is
     right, but on the light theme it's #101114 and a near-black blob
     lands like an inkblot on a white card -- far heavier than the empty
     state it's introducing. So the light theme gets a mid grey instead,
     and only the light theme changes: the dark values below are the same
     --text / --text-secondary pair the mascot already used.
     -body is the silhouette, -detail is the props drawn on and around it
     (sweatband, crumbs, speed lines, the podium block).

     -detail has to clear two backgrounds at once, which is what pins it
     as dark as it is: it's the sweatband drawn ON the body (#6b6d78) and
     also the speed lines, crumbs and podium outline drawn on the card
     (#ffffff). Going lighter than the body would suit the first and
     disappear against the second, so it goes darker and satisfies both.
     Still a charcoal grey, not the near-black --text this used to be.

     The two themes are NOT symmetric, and it's worth knowing which way.
     Measured: detail-on-body is 1.98:1 on light but 3.15:1 on dark, so the
     sweatband in the `lift` pose is the softest thing in the set on light
     and the first element to suffer if the art is ever scaled down. The
     card side has enormous headroom either way (detail-on-card is 9.7:1 on
     light), so the room to fix it is all in making -detail darker. */
  --rc-mascot-body: #6b6d78;
  --rc-mascot-detail: #3f414a;
  --sidebar-active: #f1f2f5;
  --green: #1fa971;
  --green-bg: #e7f6ee;
  --red: #d1453b;
  --red-bg: #fdeceb;
  --amber: #b9832a;
  --amber-bg: #fbf1e2;
  --blue: #2f66e8;
  --blue-bg: #eaf0fd;
  /* Link text on a surface: the Terms/Privacy consent notice, the log in /
     sign up switch, and the keyboard focus rings.

     It exists because --blue measures 3.39:1 against --card-bg on the dark
     theme -- an AA failure on the one piece of text the user is being asked
     to agree to. The dark block overrides it; see there.

     Deliberately the same LITERAL as --blue rather than var(--blue): --blue
     is also a fill under white text (.cta-blue and friends), so the two want
     to move independently. Retuning --blue for a fill must not silently
     restyle every policy link. */
  --link: #2f66e8;
  --purple: #7c4fe0;
  --purple-bg: #f3edfd;
  /* Sixth accent. Challenges used to share amber with "Log a workout",
     which broke colour-as-identifier in the quick-actions grid. Magenta
     sits in the one wide gap left in the palette (between purple ~262deg
     and red ~4deg), so it can't be mistaken for any existing accent. */
  --pink: #c0398c;
  --pink-bg: #fbeaf4;
  /* The near-black "hero" surface (Today's plan card, dark accents) --
     deliberately NOT pure black so it still reads as a card, and given
     its own tokens so the dark theme can flip it to a lighter contrast
     surface instead of disappearing into the page background. */
  --dark-card: #17181d;
  --dark-card-soft: #26272e;
  --dark-card-text: #ffffff;
  --dark-card-text-secondary: #9a9ca6;
  --radius-lg: 20px;
  --radius-md: 14px;
  /* Height of the app viewport. On a phone that is the real viewport; on
     a wide screen the box redefines it, so anything sized to a fraction of
     the screen tracks the box instead of the desktop window. Every rule
     that used vh for layout now derives from this.

     dvh, not vh: on mobile Safari `100vh` is the LARGE viewport -- the one
     you get with the URL bar hidden -- so anything pinned to the bottom of
     a 100vh box sits 60-90px below the fold while the bar is showing. The
     coach composer used to spell out `100dvh` for exactly this reason, and
     folding that rule into --app-h silently downgraded it. Declared twice
     because `dvh` is Safari 15.4+: an older engine drops the second line
     as invalid and keeps the vh fallback, which is the same
     belt-and-braces pattern nutrition.html and hyrox.css already use. */
  --app-h: 100vh;
  --app-h: 100dvh;
  --shadow: 0 1px 2px rgba(16, 17, 20, 0.03), 0 8px 24px rgba(16, 17, 20, 0.05);
}

:root[data-theme="dark"] {
  --bg: #0b0b0c;
  --card-bg: #1c1c1e;
  --text: #f5f5f7;
  --text-secondary: #8e8e93;
  --border: #2c2c2e;
  /* Unchanged from what the mascot rendered before these tokens existed
     -- the dark theme was already correct, only the light one wasn't. */
  --rc-mascot-body: #f5f5f7;
  --rc-mascot-detail: #8e8e93;
  --sidebar-active: #262628;
  --green-bg: #12321f;
  --red-bg: #3a1f1e;
  --amber-bg: #3a2f14;
  --blue-bg: #14213f;
  /* 6.20:1 on --card-bg and 7.17:1 on --bg, against --blue's failing
     3.39:1. Only lightened enough to clear AA with headroom -- it still
     reads as the same blue. --blue itself is deliberately NOT changed
     here: it is also used as a FILL under white text (.cta-blue and
     friends), where lightening it would break that pairing instead. */
  --link: #6d9bf5;
  --purple-bg: #2a1f45;
  --pink-bg: #3a1830;
  --dark-card: #232329;
  --dark-card-soft: #333440;
  --dark-card-text: #f5f5f7;
  --dark-card-text-secondary: #9a9ca6;
  --shadow: 0 1px 2px rgba(0, 0, 0, 0.3), 0 10px 30px rgba(0, 0, 0, 0.45);
}

* { box-sizing: border-box; }

/* Smooth color/theme transitions everywhere, plus tactile press feedback
   on anything clickable, without needing per-component rules. */
*, *::before, *::after {
  transition: background-color 0.18s ease, color 0.18s ease, border-color 0.18s ease, box-shadow 0.18s ease;
}

button, a, input, select, [role="button"], .exercise-search-result-item, .day-tab {
  transition: background-color 0.18s ease, color 0.18s ease, border-color 0.18s ease,
              box-shadow 0.18s ease, transform 0.1s ease, opacity 0.18s ease;
}

button:active, a.btn-primary:active, [role="button"]:active {
  transform: scale(0.96);
}

body {
  margin: 0;
  font-family: "Inter", -apple-system, "Segoe UI", Roboto, Arial, sans-serif;
  font-size: 17px;
  background: var(--bg);
  color: var(--text);
  -webkit-font-smoothing: antialiased;
  /* Tabular lining figures everywhere. DESIGN.md has specified this since
     the start ("numeric values that line up in columns should use
     tabular-nums") but nothing in the app actually set it, so every
     counter, rank, macro and timer shifted width as its digits changed --
     a 1 is much narrower than a 0 in Inter's default proportional figures.
     Setting it on `body` instead of per-component means new numeric UI
     inherits it rather than having to remember. Opt out with
     `.num-proportional` on the rare bit of running prose where uniform
     digit widths look too wide. */
  font-variant-numeric: tabular-nums lining-nums;
  font-feature-settings: "tnum" 1, "lnum" 1;
  /* Safety net: a `flex-wrap: nowrap` row with `overflow-x: auto` and
     non-shrinking children (a horizontal-scroll pattern used in a few
     places) can, if its content ever doesn't fit, expand the whole
     mobile layout viewport instead of scrolling internally -- shrinking
     every bit of UI and making fixed-size elements look oversized. Once
     that happens mobile browsers don't reliably shrink back on their
     own. This clips any such runaway overflow at the page root so one
     misbehaving component can't blow up the entire page's zoom level. */
  overflow-x: hidden;
}

/* When Thai is selected, prefer SF Pro TH (Apple's Thai system face) with
   Thai-capable fallbacks so Thai glyphs render in a proper Thai typeface
   rather than Inter's fallback. i18n.js stamps data-lang on <html>. */
:root[data-lang="th"] body,
:root[data-lang="th"] input,
:root[data-lang="th"] textarea,
:root[data-lang="th"] button,
:root[data-lang="th"] select {
  font-family: "SF Pro TH", "SF Thonburi", "Thonburi", "Noto Sans Thai",
               "Sarabun", -apple-system, "Segoe UI", Roboto, Arial, sans-serif;
}

.app {
  display: flex;
  min-height: var(--app-h);
  gap: 20px;
  padding: 20px;
  align-items: flex-start;
  /* Guard against any single over-wide child (a chart, a long unbreakable
     string, a non-wrapping flex row) forcing the whole page wider than the
     screen. Without this, such overflow makes iOS Safari zoom the entire
     page out to fit -- the "screen suddenly shrinks and shifts left" bug.
     `clip` (not `hidden`) so it never becomes a scroll container and
     position:sticky descendants keep working. Wide content that genuinely
     needs scrolling must use its own overflow-x:auto container. */
  overflow-x: clip;
  transition: transform 0.46s cubic-bezier(0.32, 0.72, 0, 1),
              border-radius 0.46s cubic-bezier(0.32, 0.72, 0, 1),
              filter 0.46s ease;
}

/* ---------- Bottom sheets: classic iOS card-sheet recede ----------
   Shared by every bottom sheet in the app (the "+" quick-actions sheet,
   weight-log, food/exercise logging, food-photo analysis, weekly check-in,
   the Hyrox leaderboard sheet): base.html's openBottomSheet()/
   closeBottomSheet() toggle .pc-sheet-active/.pc-sheet-locked on <html>
   for every one of them, so this single rule gives them all the same
   motion -- the page behind the sheet scales back, rounds its corners,
   and dims, exactly like a native iOS sheet. */
html.pc-sheet-locked, html.pc-sheet-locked body { overflow: hidden; }
/* Inside the device frame `.app` is the scroller, so freezing html/body
   alone leaves the page scrolling behind an open sheet. */
html.pc-sheet-locked .app, html.pc-sheet-locked .auth-wrap { overflow: hidden; }
html.pc-sheet-active .app {
  transform: scale(0.92) translateY(6px);
  transform-origin: top center;
  border-radius: 22px;
  overflow: hidden;
  filter: brightness(0.7);
}
@media (prefers-reduced-motion: reduce) {
  .app { transition: none; }
}

/* Escape hatch for the body-level tabular figures above. */
.num-proportional { font-variant-numeric: proportional-nums; font-feature-settings: "pnum" 1; }

/* ---------- Sidebar ---------- */

.sidebar {
  width: 220px;
  flex-shrink: 0;
  background: var(--card-bg);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow);
  padding: 20px 14px;
  position: sticky;
  top: 20px;
}

.brand {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 10px 10px 28px;
}

.brand-logo {
  height: 80px;
  width: auto;
  display: block;
}

/* Both classes point at the same logo file, a black wordmark with a
   transparent background, so inverting it (black -> white, alpha
   untouched) is all that's needed to read cleanly against the dark
   theme too. Only apply this to that wordmark -- reusing either class
   name on a different image would invert its colors too. */
:root[data-theme="dark"] .brand-logo,
:root[data-theme="dark"] .ob-logo {
  filter: invert(1);
}

.nav {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: 2px;
}

.nav a {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 10px 12px;
  border-radius: 10px;
  color: var(--text-secondary);
  text-decoration: none;
  font-size: 14px;
  font-weight: 500;
}

.nav a svg { flex-shrink: 0; }

.nav a.active {
  background: var(--sidebar-active);
  color: var(--text);
  font-weight: 600;
}

.account-block {
  margin: 14px 12px 0;
  padding-top: 14px;
  border-top: 1px solid var(--border);
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 8px;
}
.account-popup-wrap { position: relative; min-width: 0; }
.account-info {
  display: flex; align-items: center; gap: 8px; min-width: 0;
  text-decoration: none; color: inherit; border-radius: 10px;
  background: none; border: none; padding: 0; cursor: pointer; font: inherit;
  width: 100%;
}
.account-info:hover .account-name { text-decoration: underline; }

.account-popup {
  position: absolute;
  bottom: calc(100% + 8px);
  left: 0;
  min-width: 190px;
  background: var(--card-bg);
  border: 1px solid var(--border);
  border-radius: 12px;
  box-shadow: var(--shadow);
  padding: 6px;
  display: none;
  flex-direction: column;
  z-index: 50;
}
.account-popup.is-open { display: flex; }
.account-popup a {
  display: flex; align-items: center; gap: 10px;
  padding: 8px 10px; border-radius: 8px;
  font-size: 13px; font-weight: 600; color: var(--text);
  text-decoration: none;
}
.account-popup a:hover { background: var(--sidebar-active); }
.account-popup a svg { flex-shrink: 0; color: var(--text-secondary); }
.account-avatar {
  width: 26px;
  height: 26px;
  border-radius: 50%;
  background: var(--blue-bg);
  color: var(--blue);
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 12px;
  font-weight: 700;
  flex-shrink: 0;
}
.account-name {
  font-size: 12px;
  font-weight: 600;
  color: var(--text);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.account-logout-btn, .account-login-btn {
  padding: 6px 12px;
  border-radius: 999px;
  border: 1px solid var(--border);
  background: none;
  color: var(--text-secondary);
  font-size: 11px;
  font-weight: 700;
  cursor: pointer;
  font-family: inherit;
  text-decoration: none;
  flex-shrink: 0;
}
.account-logout-btn:hover, .account-login-btn:hover { border-color: var(--text-secondary); color: var(--text); }
.account-login-btn { width: 100%; text-align: center; }

/* ---------- Main content ---------- */

.main {
  flex: 1;
  min-width: 0;
  max-width: 900px;
}

.topbar {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  margin-bottom: 20px;
  padding: 4px 4px 0;
}

.topbar h1 {
  font-size: 28px;
  font-weight: 800;
  letter-spacing: -0.03em;
  margin: 0;
}

/* Circular initial avatar shown on the right of the topbar (reference
   design's greeting row) -- same visual as the sidebar's account avatar,
   just bigger and page-level. */
.topbar-avatar {
  width: 44px;
  height: 44px;
  border-radius: 50%;
  background: var(--blue-bg);
  color: var(--blue);
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 17px;
  font-weight: 700;
  flex-shrink: 0;
  align-self: center;
  text-decoration: none;
}

/* ---------- Cards ---------- */

.card {
  background: var(--card-bg);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow);
  padding: 28px;
}

.card + .card { margin-top: 20px; }

.card h2 {
  font-size: 15px;
  font-weight: 700;
  margin: 0 0 18px;
}

/* ---------- Analyze results: hero + per-stat icon chips ----------
   Shared between templates/index.html (AJAX result view) and
   templates/result.html (server-rendered fallback) -- same reason the
   .ac-* chat widget CSS lives here instead of duplicated in both. */
/* "How to perform it right" tutorial video, at the top of the results.
   Shared by the AJAX result view and the server-rendered result.html. */
.an-tutorial-card { padding: 12px; }
.an-tutorial-head {
  display: flex; align-items: center; gap: 9px;
  margin-bottom: 11px; padding: 0 2px;
}
.an-tutorial-head-icon {
  width: 30px; height: 30px; border-radius: 9px;
  background: rgba(255, 0, 0, 0.10); color: #ff0000;
  display: flex; align-items: center; justify-content: center; flex-shrink: 0;
}
.an-tutorial-head-icon svg { width: 17px; height: 17px; }
.an-tutorial-head-text { min-width: 0; }
.an-tutorial-title { font-size: 14px; font-weight: 700; color: var(--text); line-height: 1.25; }
.an-tutorial-sub { font-size: 11.5px; color: var(--text-secondary); margin-top: 1px; }
.an-tutorial-frame {
  position: relative; width: 100%; aspect-ratio: 16 / 9;
  border-radius: 14px; overflow: hidden; background: #000;
}
.an-tutorial-frame iframe { position: absolute; inset: 0; width: 100%; height: 100%; border: 0; }
.an-tutorial-search {
  display: flex; align-items: center; justify-content: center; gap: 8px;
  padding: 13px; border-radius: 14px;
  background: var(--card-bg); border: 1px solid var(--border);
  color: var(--text); font-size: 13px; font-weight: 600; text-decoration: none;
}
.an-tutorial-search:hover { border-color: var(--text-secondary); }
.an-tutorial-search svg { width: 16px; height: 16px; color: #ff0000; flex-shrink: 0; }

/* One number, not four: the overall score is the only score shown (the
   stretch/squeeze sub-scores and rep count used to sit alongside it in a
   4-tile row, which was a lot of small boxes competing for attention on
   a phone screen for what's really one headline fact). A big ring for
   the number, a plain label under it, then the one-line verdict and
   optional favor-note directly below, all in one calm centered card --
   the rest of the detail lives one tap away in the breakdown below. */
.an-hero-card { text-align: center; }
.an-score-ring {
  width: 108px; height: 108px; margin: 4px auto 14px;
  border-radius: 50%;
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  background: var(--bg);
  border: 4px solid var(--border);
}
.an-score-value { font-size: 36px; font-weight: 800; letter-spacing: -0.02em; line-height: 1; color: var(--text); }
.an-score-outof { font-size: 11px; font-weight: 700; color: var(--text-secondary); margin-top: 3px; }
.an-score-ring.score-good { border-color: var(--green); }
.an-score-ring.score-good .an-score-value { color: var(--green); }
.an-score-ring.score-ok { border-color: var(--amber); }
.an-score-ring.score-ok .an-score-value { color: var(--amber); }
.an-score-ring.score-bad { border-color: var(--red); }
.an-score-ring.score-bad .an-score-value { color: var(--red); }
.an-score-label {
  font-size: 12.5px; font-weight: 700; color: var(--text-secondary);
  text-transform: uppercase; letter-spacing: 0.04em;
}
.an-hero-card .fb-verdict { justify-content: center; max-width: 320px; margin: 14px auto 0; padding: 0; }
.an-hero-card .favor-note { text-align: left; max-width: 320px; margin-left: auto; margin-right: auto; }

/* ---------- Form ---------- */

.field { margin-top: 18px; }
.field:first-child { margin-top: 0; }

.field label {
  display: block;
  font-weight: 600;
  font-size: 13px;
  margin-bottom: 8px;
}

.field .hint {
  font-size: 12px;
  color: var(--text-secondary);
  margin-top: 6px;
}

input[type="file"], select {
  width: 100%;
  padding: 11px 14px;
  border: 1px solid var(--border);
  background: var(--bg);
  border-radius: var(--radius-md);
  font-size: 14px;
  font-family: inherit;
  color: var(--text);
}

.btn-primary {
  margin-top: 22px;
  width: 100%;
  padding: 14px;
  border: none;
  border-radius: 999px;
  background: linear-gradient(135deg, #4d7ff5, var(--blue));
  color: #fff;
  font-size: 14px;
  font-weight: 700;
  cursor: pointer;
  font-family: inherit;
  transition: opacity 0.15s ease, transform 0.1s ease;
  /* Also used on an <a> (result.html's fallback CTA), not just <button> */
  display: block;
  text-decoration: none;
  text-align: center;
  box-sizing: border-box;
}

.btn-primary:hover { opacity: 0.92; transform: translateY(-1px); }

.btn-primary:active { transform: translateY(0); }

.error {
  background: var(--red-bg);
  color: var(--red);
  padding: 12px 16px;
  border-radius: var(--radius-md);
  margin-bottom: 18px;
  font-size: 13px;
  font-weight: 500;
}

/* ---------- Streak icons (sprite defined in base.html) ----------
   Replaces the 🔥 / 🏆 emoji, which rendered differently per OS and could not
   follow the theme. Sized in em so every call site keeps its own scale from
   whatever font-size it already sets: 22px in .st-stat-icon, 13px in the home
   and challenges streak chips, 12px in .pc-streak-badge. 1.05em because an
   emoji glyph draws slightly larger than its em box, and matching that keeps
   the chips from looking like the flame shrank. */

.rc-icon {
  width: 1.05em;
  height: 1.05em;
  display: block;
  flex-shrink: 0;
  fill: currentColor;
}

/* ---------- Exercise icon (used in the selected-exercise chip) ---------- */

.exercise-icon {
  width: 32px;
  height: 32px;
  border-radius: 8px;
  background: var(--card-bg);
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 16px;
  flex-shrink: 0;
  overflow: hidden;
}

/* Per-exercise illustration from the icon pack (see exercise_icons.py) --
   shared across every page that shows a per-exercise thumbnail. */
.ex-icon-img { width: 100%; height: 100%; object-fit: contain; display: block; }
.ex-modal-result-icon {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 32px;
  height: 32px;
  border-radius: 8px;
  flex-shrink: 0;
  background: var(--bg);
  font-size: 16px;
  overflow: hidden;
}

/* ---------- Form breakdown ----------
   A single-column list, one section per row -- a 2-column grid of
   differently-colored tinted tiles read as a "wall of color" once there
   were 5-6 sections on screen at once, which is exactly the "too much
   going on" feeling this was rewritten to fix. Every row now shares the
   same plain background; the only color left is the small icon chip and
   the section title, which is plenty to tell categories apart without
   turning the page into a color grid. Full-width rows also give each
   summary sentence room to read on one or two lines instead of wrapping
   awkwardly in a half-width tile. */
.fb-head { margin-bottom: 8px; }
.fb-head-title { font-size: 17px; font-weight: 800; letter-spacing: -0.01em; line-height: 1.35; }
.fb-head-sub { font-size: 12.5px; color: var(--text-secondary); margin-top: 5px; font-weight: 500; }

.fb-list { display: flex; flex-direction: column; gap: 10px; }

/* One-line verdict tying the score to what the sections show: a small
   colored dot + short colored sentence, no filled banner. Generous
   padding on mobile so it doesn't read as glued to the subhead above or
   the cards below -- this is the line that sets up the whole breakdown,
   so it needs room to breathe like a paragraph, not a label. */
.fb-verdict {
  display: flex; align-items: flex-start; gap: 8px;
  font-size: 15px; font-weight: 600; line-height: 1.55;
  padding: 12px 2px 22px;
}
.fb-verdict::before {
  content: ""; width: 7px; height: 7px; border-radius: 50%;
  flex-shrink: 0; margin-top: 6px;
}
.fb-verdict-bad { color: var(--red); }
.fb-verdict-bad::before { background: var(--red); }
.fb-verdict-ok { color: var(--amber); }
.fb-verdict-ok::before { background: var(--amber); }
.fb-verdict-good { color: var(--green); }
.fb-verdict-good::before { background: var(--green); }

.fb-item {
  position: relative;
  border-radius: 16px;
  background: var(--bg);
}

/* "Muted" sections are shown but deliberately secondary -- e.g. the limited
   positives on a poor set, or the minor notes on a great one. Kept legible
   (not near-invisible), just clearly lower-priority than the featured ones. */
.fb-item-muted { opacity: 0.62; }

/* Icon-beside-text row, full width -- the only layout now that the list
   is single-column. Padding widened (mobile-first -- this app is used
   almost exclusively on phones) so nothing feels shrink-wrapped against
   the row's edge. */
.fb-item-main {
  display: flex; flex-direction: row; align-items: flex-start; gap: 14px;
  width: 100%; padding: 16px;
  background: none; border: none; text-align: left; font-family: inherit; color: var(--text);
}
.fb-item.is-expandable .fb-item-main { cursor: pointer; padding-right: 38px; }
.fb-item.is-expandable .fb-item-main:active { transform: scale(0.98); }

.fb-item-icon {
  width: 34px; height: 34px; border-radius: 11px; flex-shrink: 0;
  display: flex; align-items: center; justify-content: center; color: #fff;
}
.fb-item-icon svg { width: 17px; height: 17px; display: block; }

.fb-item-text { flex: 1; min-width: 0; }
.fb-item-title {
  font-size: 10.5px; font-weight: 800; margin-bottom: 7px;
  text-transform: uppercase; letter-spacing: 0.05em;
}
.fb-item-summary { font-size: 15px; font-weight: 500; line-height: 1.55; color: var(--text); }
.fb-item-summary p { margin: 0; }

.fb-item-chevron {
  position: absolute; top: 16px; right: 14px;
  color: var(--text-secondary); opacity: 0.7;
  transition: transform 0.25s ease;
}
.fb-item.is-expanded .fb-item-chevron { transform: rotate(180deg); }

/* A visible rule (not just padding) so the expanded detail reads as a
   distinct "here's more" block under the headline, not text that just
   continues on with no boundary -- the zero top-padding this used to
   have made the two run together. */
.fb-item-detail { display: none; padding: 14px 16px 18px; border-top: 1px solid var(--border); }
.fb-item.is-expanded .fb-item-detail { display: block; animation: fb-reveal 0.24s ease; }
.fb-item-detail ul { margin: 0; padding-left: 17px; }
.fb-item-detail li { font-size: 15px; line-height: 1.65; color: var(--text); margin-bottom: 10px; }
.fb-item-detail li:last-child { margin-bottom: 0; }
.fb-item-detail p { margin: 0; font-size: 15px; line-height: 1.65; }
@keyframes fb-reveal { from { opacity: 0; transform: translateY(-4px); } to { opacity: 1; transform: none; } }

/* "Full report" -- reveals the secondary sections (see index.html's
   FB_ALWAYS_VISIBLE / result.html's matching Jinja filter), which used to
   all render at once and read as a wall of cards. Styled as a plain,
   low-emphasis row (not a button-primary) since it's a "see more," not a
   call to action. */
.fb-full-report-toggle {
  display: flex; align-items: center; justify-content: center; gap: 6px;
  width: 100%; margin-top: 10px; padding: 12px;
  border: none; border-radius: 12px;
  background: var(--bg); color: var(--text-secondary);
  font-family: inherit; font-size: 13.5px; font-weight: 700;
  cursor: pointer;
}
.fb-full-report-toggle:hover { color: var(--text); }
.fb-full-report-toggle:active { transform: scale(0.98); }
.fb-full-report-toggle svg { transition: transform 0.2s ease; }
.fb-full-report-toggle.is-open svg { transform: rotate(180deg); }
.fb-list-secondary { margin-top: 10px; }
.fb-list-secondary[hidden] { display: none; }

/* Every row shares the same plain background now (see .fb-item) -- color
   lives only in the small solid icon chip and the section label, not a
   filled tile, so 5-6 sections on screen read as one calm list instead
   of a grid of different-colored boxes. */
.fb-summary .fb-item-icon { background: var(--text-secondary); }
.fb-positives .fb-item-icon { background: var(--green); }
.fb-negatives .fb-item-icon { background: var(--red); }
.fb-improvements .fb-item-icon { background: var(--blue); }
.fb-injury .fb-item-icon { background: var(--amber); }
.fb-progress .fb-item-icon { background: var(--purple); }

.fb-summary .fb-item-title { color: var(--text-secondary); }
.fb-positives .fb-item-title { color: var(--green); }
.fb-negatives .fb-item-title { color: var(--red); }
.fb-improvements .fb-item-title { color: var(--blue); }
.fb-injury .fb-item-title { color: var(--amber); }
.fb-progress .fb-item-title { color: var(--purple); }

.pill-badge {
  display: inline-flex;
  align-items: center;
  padding: 3px 10px;
  border-radius: 999px;
  background: var(--green-bg);
  color: var(--green);
  font-size: 12px;
  font-weight: 600;
}

.favor-note {
  margin-top: 16px;
  padding: 12px 16px;
  border-radius: var(--radius-md);
  background: var(--blue-bg);
  color: var(--blue);
  font-size: 15px;
  font-weight: 500;
}

.favor-note strong { text-transform: capitalize; }

/* ---------- Analyze results: Apple-Intelligence-style "ask" bar ----------
   Replaces the old boxed chatbot. A frosted-glass pill in normal flow at
   the top of the results; asking a question grows a translucent thread
   panel downward from it, pushing the rest of the results down to make
   room. The same AnalyzeChatWidget.init(context) drives it, shared by
   index.html (AJAX) and result.html (fallback). Scoped to one analysis,
   no persistence -- context is re-sent with every message, like
   coach_chat.py's history. */

/* "Analyze another" is a normal button at the end of the content --
   given real room above it so it reads as a deliberate next step, not
   text that happens to have a button stuck to the bottom of it. */
.an-again { display: flex; justify-content: center; margin-top: 36px; }
.an-again .btn-primary { margin-top: 0; max-width: 320px; }

/* The dock sits in normal flow in the MIDDLE at the top of the results as a
   small glass circle. Binary: the collapsed circle or the fully open chat,
   never a resting in-between. Opening (tap the circle, or pull down at the
   very top edge) widens the pill AND expands the panel below it; since it's
   in flow the results are pushed down to make room, and closing returns
   everything to exactly where it was. */
.ag-dock {
  width: 100%;
  display: flex;
  flex-direction: column;
  align-items: center;
  margin: 2px 0 8px;
  /* transform is used only for the pull-preview dip; snaps back smoothly. */
  transition: transform 0.32s cubic-bezier(0.22, 1, 0.36, 1);
}
.ag-dock.is-pulling { transition: none; } /* follow the finger 1:1 while pulling */

/* The pill: collapsed it's a small, quiet centered circle. On .is-open it
   widens into the full centered bar; overflow-hidden clips the input row
   until it's grown. A slow, subtle breathing glow on the collapsed circle
   so the dock doesn't read as just an inert icon -- recognizable as
   something to tap, without any loud color or permanent motion once open. */
.ag-bar {
  align-self: center;
  position: relative;
  overflow: hidden;
  width: 44px;
  height: 44px;
  display: flex; align-items: center;
  border-radius: 999px;
  background: color-mix(in srgb, var(--card-bg) 42%, transparent);
  -webkit-backdrop-filter: blur(22px) saturate(1.6);
  backdrop-filter: blur(22px) saturate(1.6);
  border: 1px solid color-mix(in srgb, var(--text) 9%, transparent);
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
  animation: ag-bar-breathe 3.6s ease-in-out infinite;
  transition: width 0.44s cubic-bezier(0.22, 1, 0.36, 1),
              box-shadow 0.34s ease, border-color 0.2s ease,
              background-color 0.34s ease;
}
.ag-dock.is-open .ag-bar {
  width: min(480px, 100%);
  height: 52px;
  background: color-mix(in srgb, var(--card-bg) 55%, transparent);
  border-color: color-mix(in srgb, var(--text) 12%, transparent);
  box-shadow: 0 14px 40px rgba(0, 0, 0, 0.22);
  animation: none;
}
@keyframes ag-bar-breathe {
  0%, 100% { box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08); }
  50% { box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08), 0 0 0 5px color-mix(in srgb, var(--purple) 14%, transparent); }
}

/* Small persistent caption under the dock naming what it is -- the bare
   sparkle icon alone reads as decoration more than a button; a couple
   words of muted text fix that without adding any color or size. Fades
   out once open, when the input/panel take over the same job. */
.ag-dock-label {
  margin-top: 6px;
  font-size: 11.5px; font-weight: 600;
  color: var(--text-secondary);
  opacity: 0.8;
  transition: opacity 0.2s ease;
}
.ag-dock.is-open .ag-dock-label { opacity: 0; pointer-events: none; }

/* The sparkle circle IS the toggle button: tap to open, tap again to close. */
.ag-spark {
  flex-shrink: 0;
  width: 44px; height: 44px; padding: 0;
  display: flex; align-items: center; justify-content: center;
  background: none; border: none; cursor: pointer;
  color: color-mix(in srgb, var(--text-secondary) 80%, transparent);
  transition: color 0.2s ease, width 0.3s ease, height 0.3s ease;
}
.ag-spark svg { width: 17px; height: 17px; }
.ag-dock.is-open .ag-spark { width: 52px; height: 52px; color: var(--purple); }
.ag-dock.is-open .ag-spark svg { width: 19px; height: 19px; }
.ag-spark:active { opacity: 0.6; }

.ag-inputrow {
  flex: 1; min-width: 0; align-self: stretch;
  display: flex; align-items: center; gap: 8px;
  padding-right: 7px;
  opacity: 0;
  transition: opacity 0.3s ease;
}
.ag-dock.is-open .ag-inputrow { opacity: 1; }
.ag-inputrow input {
  flex: 1; min-width: 0;
  height: 100%;
  background: none; border: none; outline: none;
  color: var(--text); font-family: inherit;
  /* 16px so iOS Safari doesn't zoom the page on focus. */
  font-size: 16px;
}
/* This input carried `outline: none` with nothing replacing it anywhere --
   unlike .log-sheet-search, .pc-ck-weight-field and .hx-space-input-wrap,
   which all light their wrapper on :focus-within. So tabbing into the
   analyze chat box moved focus with no visible change at all (WCAG 2.4.7).
   The ring goes on the input itself because .ag-inputrow has no background
   of its own to outline. */
.ag-inputrow input:focus-visible {
  outline: 2px solid var(--link);
  outline-offset: 2px;
  border-radius: 6px;
}
.ag-inputrow input::placeholder { color: var(--text-secondary); }
.ag-send {
  flex-shrink: 0;
  width: 44px; height: 44px; border-radius: 50%; border: none;
  background: linear-gradient(135deg, #9b7bef, var(--purple));
  color: #fff; cursor: pointer;
  display: flex; align-items: center; justify-content: center;
  transition: opacity 0.15s ease, transform 0.1s ease;
}
.ag-send:hover { opacity: 0.92; }
.ag-send:active { transform: scale(0.9); }
.ag-send:disabled { opacity: 0.4; cursor: not-allowed; }

/* Collapsible panel below the bar (chips / thread / status). Animating
   grid-template-rows 0fr<->1fr expands/collapses its height regardless of
   content, so the reflow that pushes the page down (and back) is smooth. */
.ag-panel {
  width: 100%; max-width: 560px;
  display: grid;
  grid-template-rows: 0fr;
  transition: grid-template-rows 0.44s cubic-bezier(0.22, 1, 0.36, 1);
}
.ag-dock.is-open .ag-panel { grid-template-rows: 1fr; }
.ag-panel-inner {
  min-height: 0;
  overflow: hidden;
  display: flex; flex-direction: column; gap: 10px;
}
.ag-dock.is-open .ag-panel-inner { padding-top: 10px; }
@media (prefers-reduced-motion: reduce) {
  .ag-bar, .ag-panel { transition: none; animation: none; }
}

/* Conversation thread: see-through glass panel below the bar. */
.ag-thread {
  width: 100%;
  max-height: calc(var(--app-h) * 0.46); overflow-y: auto;
  display: flex; flex-direction: column; gap: 10px;
  padding: 14px;
  border-radius: 22px;
  background: color-mix(in srgb, var(--card-bg) 52%, transparent);
  -webkit-backdrop-filter: blur(26px) saturate(1.7);
  backdrop-filter: blur(26px) saturate(1.7);
  border: 1px solid color-mix(in srgb, var(--text) 9%, transparent);
  box-shadow: 0 18px 50px rgba(0, 0, 0, 0.22);
  scroll-behavior: smooth;
}
.ag-thread:empty { display: none; }

/* Tappable starter questions shown under the bar until the first message. */
.ag-suggest {
  display: flex; flex-wrap: wrap; justify-content: center; gap: 8px;
  width: 100%;
}
.ag-suggest:empty { display: none; }
.ag-chip {
  border: 1px solid color-mix(in srgb, var(--text) 12%, transparent);
  background: color-mix(in srgb, var(--card-bg) 60%, transparent);
  -webkit-backdrop-filter: blur(18px) saturate(1.6);
  backdrop-filter: blur(18px) saturate(1.6);
  color: var(--text-secondary);
  font-family: inherit; font-size: 12.5px; font-weight: 600;
  padding: 8px 14px; border-radius: 999px; cursor: pointer;
  transition: color 0.15s ease, border-color 0.15s ease, transform 0.1s ease;
}
.ag-chip:hover { color: var(--text); border-color: color-mix(in srgb, var(--purple) 55%, transparent); }
.ag-chip:active { transform: scale(0.96); }

/* Rate-limit / status line under the bar; hidden when empty. */
.ag-sub { font-size: 11px; color: var(--text-secondary); text-align: center; }
.ag-sub:empty { display: none; }

/* Thread bubbles: see-through glass, matching the bar. Each new one rises
   in softly. The user's is a translucent purple tint; the assistant's is a
   translucent neutral -- both keep the page readable through them. */
.ag-row { display: flex; max-width: 90%; animation: ag-rise 0.32s cubic-bezier(0.22, 1, 0.36, 1); }
.ag-row-user { align-self: flex-end; }
.ag-row-assistant { align-self: flex-start; }
.ag-bubble {
  padding: 9px 13px; border-radius: 16px;
  font-size: 15px; line-height: 1.5; word-break: break-word;
  -webkit-backdrop-filter: blur(16px) saturate(1.6);
  backdrop-filter: blur(16px) saturate(1.6);
  border: 1px solid color-mix(in srgb, var(--text) 8%, transparent);
}
.ag-row-assistant .ag-bubble {
  background: color-mix(in srgb, var(--card-bg) 42%, transparent);
  color: var(--text);
  border-bottom-left-radius: 5px;
}
.ag-row-user .ag-bubble {
  background: color-mix(in srgb, var(--purple) 52%, transparent);
  border-color: color-mix(in srgb, var(--purple) 40%, transparent);
  color: #fff;
  border-bottom-right-radius: 5px;
}
.ag-bubble p { margin: 0 0 6px; }
.ag-bubble p:last-child { margin: 0; }
.ag-bubble strong { font-weight: 700; }
.ag-bullet-list { margin: 4px 0 6px; padding-left: 16px; }
.ag-bullet-list li { margin-bottom: 3px; }
.ag-bullet-list li:last-child { margin-bottom: 0; }
/* Thinking indicator: same glass as an assistant bubble, not a flat grey
   block, so the "thinking" moment reads as part of the transparent UI. */
.ag-typing {
  align-self: flex-start; display: flex; gap: 4px; padding: 12px 14px;
  background: color-mix(in srgb, var(--card-bg) 42%, transparent);
  -webkit-backdrop-filter: blur(16px) saturate(1.6);
  backdrop-filter: blur(16px) saturate(1.6);
  border: 1px solid color-mix(in srgb, var(--text) 8%, transparent);
  border-radius: 16px; border-bottom-left-radius: 5px;
  animation: ag-rise 0.32s cubic-bezier(0.22, 1, 0.36, 1);
}
.ag-typing span { width: 6px; height: 6px; border-radius: 50%; background: var(--text-secondary); animation: ag-bounce 1.2s ease-in-out infinite; }
.ag-typing span:nth-child(2) { animation-delay: 0.15s; }
.ag-typing span:nth-child(3) { animation-delay: 0.3s; }

@keyframes ag-rise {
  from { opacity: 0; transform: translateY(6px); }
  to { opacity: 1; transform: none; }
}
@media (prefers-reduced-motion: reduce) {
  .ag-row, .ag-typing { animation: none; }
}

@keyframes ag-bounce {
  0%, 60%, 100% { transform: translateY(0); opacity: 0.5; }
  30% { transform: translateY(-3px); opacity: 1; }
}

/* ---------- Bottom tab bar + FAB ---------- */
/* These used to be phone-only, with desktop getting the sidebar layout
   instead. RepCheck is now phone-shaped at every width: on a wide screen
   the app is drawn inside a device frame (see "Device frame" at the end of
   this file) rather than being re-laid-out as a desktop app. So the phone
   layout below is unconditional, and `@media all` is deliberate -- it
   keeps the block's indentation and history intact rather than reflowing
   250 lines to delete one line. */
@media all {
  /* align-items: stretch (not the base rule's flex-start) so `main` fills
     the column's full width. With flex-start in a column flex, main is
     sized to its content's max-content width instead of the screen, which
     is what let a wide inner element push main past the viewport edge and
     trigger the iOS zoom-out. */
  /* padding-bottom clears the floating tab bar: it sits at bottom:12px and
     the "+" bubble is 58px tall, so 12px of padding left the last ~58px of
     every scrollable page stranded behind it (the Home macros card was
     visibly cut off). 12 + 58 + 12 of breathing room.

     padding-top used to be able to stay a flat 12px because
     capacitor.config.json's contentInset:"always" made iOS itself push the
     whole page down by the safe-area amount before any CSS ran -- free
     protection, but paid for with a native background gutter (a literal
     black bar above the status bar/Dynamic Island) that the app's own
     background never got to paint over. Now that contentInset is "never"
     (see that file), this padding is the ONLY thing keeping the topbar's
     title and avatar clear of the notch, so 12px alone is not enough on
     any device with a taller status bar than that -- max() keeps the
     plain 12px on a browser/desktop (env() is 0px there) while growing to
     the real inset on an iPhone. */
  .app { flex-direction: column; align-items: stretch; padding: 12px; padding-top: max(12px, env(safe-area-inset-top, 0px)); gap: 12px; padding-bottom: calc(82px + env(safe-area-inset-bottom, 0px)); }
  /* Primary nav now lives in the fixed bottom tab bar instead -- the
     sidebar (brand logo + nav + account block) is the old mobile nav and
     is fully replaced by it. Previously only .nav/.account-block were
     hidden, leaving the sidebar itself (a full-width rounded card, just
     for the brand logo) rendered as a stray near-empty bar stacked above
     .topbar on every page at phone width. */
  .sidebar { display: none; }
  .card { padding: 22px; }
  .topbar h1 { font-size: 21px; }

  /* Room for the fixed bottom bar so the last card is never hidden
     behind it; the bar now floats 12px off the bottom edge, so clearance
     is its offset + pill height + breathing room, plus the iOS
     home-indicator safe area. */
  .main { padding-bottom: calc(92px + env(safe-area-inset-bottom, 0px)); }

  /* Two floating "bubbles" (per the reference mock): a fully rounded
     pill with the nav items, and the circular "+" beside it. The nav
     element itself is just an invisible flex row pinning them both. */
  .mobile-tabbar {
    display: flex;
    align-items: center;
    gap: 10px;
    position: fixed;
    left: 12px; right: 12px;
    bottom: calc(12px + env(safe-area-inset-bottom, 0px));
    z-index: 100;
    transition: opacity 0.22s ease, transform 0.22s ease;
  }
  /* Deliberately hidden (not just covered by a higher z-index) while a
     sheet is open -- an active task, like logging a set or scanning a
     food photo, shouldn't share the screen with unrelated destinations. */
  html.pc-sheet-active .mobile-tabbar {
    opacity: 0;
    pointer-events: none;
    transform: translateY(12px);
  }
  @media (prefers-reduced-motion: reduce) {
    .mobile-tabbar { transition: none; }
    /* The bubble still moves to the right tab, it just gets there
       without travelling. */
    .mt-indicator { transition: none; }
    .mt-item { transition: none; }
  }
  .mt-pill {
    flex: 1;
    min-width: 0;
    display: flex;
    align-items: stretch;
    background: var(--card-bg);
    border: 1px solid var(--border);
    border-radius: 999px;
    padding: 5px;
    box-shadow: 0 12px 32px rgba(0, 0, 0, 0.18);
    /* Anchors .mt-indicator, whose offsets nav.js measures against the
       pill (offsetLeft/offsetTop), so this must be the offset parent. */
    position: relative;
    /* Named so the pill holds perfectly still across page navigations
       (see the View Transitions block below) while the active bubble
       glides to its new slot -- no cross-fade flicker on the bar itself. */
    view-transition-name: mt-pill;
    /* nav.js now drags the indicator across the pill on a held/moving
       touch (see its pointermove handling). Without this, iOS treats
       that same gesture as a pan and fights the JS for it -- `none`
       hands the whole pill over to pointer events, no native panning
       or pinch-zoom to arbitrate against. */
    touch-action: none;
  }

  /* The active bubble, once nav.js has taken it over. It exists as its own
     element purely so it can be MOVED: a background on the active <a> can
     only blink from one tab to the next, whereas this slides on the
     compositor, and slides the instant the thumb lands rather than after
     the next page has loaded. Sized and positioned entirely from JS. */
  .mt-indicator {
    position: absolute;
    top: 0; left: 0;
    border-radius: 999px;
    background: var(--sidebar-active);
    pointer-events: none;
    /* Slight overshoot so it settles like the rest of the app's motion
       (same curve family as the "+" and the quick-action tiles) rather
       than arriving with a mechanical stop. */
    transition: transform 0.34s cubic-bezier(0.34, 1.32, 0.64, 1),
                width 0.34s cubic-bezier(0.34, 1.32, 0.64, 1),
                height 0.34s cubic-bezier(0.34, 1.32, 0.64, 1);
  }
  /* Hidden until nav.js has actually measured and placed it, so it can
     never flash at 0,0 during the gap between first paint and the
     deferred script running. */
  .mt-pill:not(.has-indicator) .mt-indicator { display: none; }
  /* Icon-only items; the active one sits in its own softly highlighted
     bubble inside the pill (like the mock's "Overview"). No labels — the
     bar stays uncluttered and the "+" carries everything else. */
  .mt-item {
    flex: 1;
    min-width: 0;
    display: flex;
    align-items: center;
    justify-content: center;
    padding: 13px 4px;
    border-radius: 999px;
    color: var(--text-secondary);
    text-decoration: none;
    background: none;
    border: none;
    cursor: pointer;
    font-family: inherit;
    /* Lifts the icons above .mt-indicator, which is painted behind them. */
    position: relative;
    z-index: 1;
    /* The colour change is left to catch up on its own rather than
       snapping, so a tapped tab lights up as the bubble arrives. */
    transition: color 0.22s ease;
    /* These are real <a href> tags, so a touch-and-hold used to trigger
       iOS's own link-preview callout -- a screenshot-style peek of the
       destination page, floating right over the app. touch-callout:none
       kills that; user-select:none stops the hold from also trying to
       select the icon/highlight it as text. Holding a tab is now just
       the start of the drag-to-switch gesture below, nothing else. */
    -webkit-touch-callout: none;
    -webkit-user-select: none;
    user-select: none;
  }
  /* Fallback highlight: what the active tab looks like when nav.js has
     not run (script blocked, or the brief moment before the deferred
     script executes). Identical geometry to .mt-indicator, so the
     hand-off below is invisible. */
  .mt-item.is-active {
    background: var(--sidebar-active); color: var(--text);
    /* The star of the show: this bubble morphs from the old page's active
       slot to the new page's on every navigation (View Transitions). */
    view-transition-name: mt-active-bubble;
  }
  /* Once nav.js owns the bubble, the static highlight has to go -- and so
     does its view-transition-name. Two elements sharing one name makes the
     browser abort the ENTIRE view transition (i.e. a hard cut back to
     exactly the reload-looking navigation this is meant to remove), so the
     name is handed over rather than duplicated. */
  .mt-pill.has-indicator .mt-item.is-active {
    background: none;
    view-transition-name: none;
  }
  .mt-pill.has-indicator .mt-indicator { view-transition-name: mt-active-bubble; }
  .mt-item:active { transform: scale(0.9); }

  /* The "+" bubble: its own raised circle beside the pill — the most
     saturated element in the bar since it's the fastest path to the
     app's core actions from anywhere. */
  .mt-fab {
    display: flex;
    align-items: center;
    justify-content: center;
    width: 58px; height: 58px;
    border-radius: 50%;
    border: none;
    background: linear-gradient(135deg, #4d7ff5, var(--blue));
    color: #fff;
    cursor: pointer;
    flex-shrink: 0;
    /* Named so it stays put across navigations, like the pill. */
    view-transition-name: mt-fab;
  }
  .mt-fab svg { transition: transform 0.28s cubic-bezier(0.34, 1.56, 0.64, 1); }
  /* wireSheet() flips aria-expanded on the FAB when the sheet opens/closes,
     so the "+" spins into an "×" purely from CSS -- no extra JS. */
  .mt-fab[aria-expanded="true"] svg { transform: rotate(135deg); }
  .mt-fab:active { transform: scale(0.92); }

  .mt-sheet-overlay {
    display: block;
    position: fixed; inset: 0;
    z-index: 200;
    opacity: 0;
    pointer-events: none;
    transition: opacity 0.18s ease;
  }
  .mt-sheet-overlay.is-open { opacity: 1; pointer-events: auto; }
  .mt-sheet {
    position: absolute;
    left: 0; right: 0; bottom: 0;
    /* Cap the height and scroll internally: the "+" sheet now carries the
       action tiles AND the folded-in "More" pages, so on shorter phones
       it can exceed the viewport -- without this the top rows (handle +
       first tiles) would be clipped off-screen with no way to reach them. */
    max-height: calc(var(--app-h) * 0.88);
    overflow-y: auto;
    background: var(--card-bg);
    border-radius: 20px 20px 0 0;
    padding: 10px 16px calc(16px + env(safe-area-inset-bottom, 0px));
    transform: translateY(100%);
    transition: transform 0.22s ease;
  }
  .mt-sheet-overlay.is-open .mt-sheet { transform: translateY(0); }
  .mt-sheet-handle { width: 36px; height: 4px; border-radius: 999px; background: var(--border); margin: 4px auto 14px; }
  .mt-sheet-title { font-size: 12px; font-weight: 700; color: var(--text-secondary); text-transform: uppercase; letter-spacing: 0.03em; margin: 0 4px 10px; }
  .mt-sheet-action {
    display: flex;
    align-items: center;
    gap: 12px;
    padding: 10px 4px;
    text-decoration: none;
    color: var(--text);
  }
  .mt-sheet-action-icon {
    display: flex; align-items: center; justify-content: center;
    width: 40px; height: 40px; border-radius: 12px; flex-shrink: 0;
    font-size: 15px; font-weight: 700;
  }
  .mt-sheet-action-title { display: block; font-size: 14px; font-weight: 700; }
  .mt-sheet-action-sub { display: block; font-size: 12px; color: var(--text-secondary); margin-top: 1px; }
  .mt-sheet-cancel {
    width: 100%; margin-top: 8px; padding: 12px;
    border-radius: 999px; border: 1px solid var(--border);
    background: none; color: var(--text); font-size: 13px; font-weight: 700;
    font-family: inherit; cursor: pointer;
  }

  /* ---------- Quick-actions sheet (the "+" FAB) redesign ----------
     A flat list of full-width rows (icon, label, chevron) that stagger in
     when the sheet opens. qa-tile is shared by both panes -- the action
     rows and the "More" pages -- so the whole sheet reads as one list. */
  .qa-sheet { padding: 10px 16px calc(18px + env(safe-area-inset-bottom, 0px)); }
  .qa-head { margin: 2px 4px 16px; display: flex; align-items: center; justify-content: space-between; gap: 12px; }
  .qa-head-title { font-size: 20px; font-weight: 800; letter-spacing: -0.015em; color: var(--text); }
  .qa-icon-btn {
    display: flex; align-items: center; justify-content: center;
    width: 32px; height: 32px; flex-shrink: 0;
    border: 1px solid var(--border); border-radius: 50%;
    background: none; color: var(--text-secondary);
    font-family: inherit; font-size: 15px; cursor: pointer;
    transition: color 0.15s ease, border-color 0.15s ease;
  }
  .qa-icon-btn:hover { color: var(--text); border-color: var(--text-secondary); }
  .qa-icon-btn:active { transform: scale(0.94); }

  .qa-rows { display: flex; flex-direction: column; gap: 10px; }
  .qa-tile {
    display: flex; align-items: center; justify-content: space-between;
    gap: 12px;
    width: 100%;
    padding: 18px;
    border-radius: 18px;
    border: none;
    background: var(--sidebar-active);
    color: var(--text);
    text-decoration: none;
    font-family: inherit;
    text-align: left;
    cursor: pointer;
    transition: background-color 0.15s ease, transform 0.1s ease;
  }
  /* No glow anywhere in this sheet: a drop shadow under a row reads as a
     halo on the dark theme, which is the effect we keep being asked to
     remove. */
  .qa-tile:hover { background: var(--border); }
  .qa-tile:active { transform: scale(0.97); }
  .qa-tile-main { display: flex; align-items: center; gap: 14px; min-width: 0; }
  .qa-tile-icon { display: flex; flex-shrink: 0; color: var(--text); }
  .qa-tile-title { font-size: 17px; font-weight: 700; line-height: 1.25; letter-spacing: -0.01em; }
  .qa-tile-chevron { display: flex; flex-shrink: 0; color: var(--text-secondary); }

  /* ---------- Sliding panes ----------
     The "More" pages used to sit in a labelled list UNDER the action tiles,
     which made the sheet tall enough to scroll on short phones. They now
     live on a second pane that slides in over the first, so only one is
     ever mounted and neither pane scrolls. .qa-panes animates its own
     height between the two so the sheet morphs instead of jumping. */
  .qa-panes { position: relative; overflow: hidden; transition: height 0.3s cubic-bezier(0.22, 1, 0.36, 1); }
  .qa-pane { transition: transform 0.3s cubic-bezier(0.22, 1, 0.36, 1), opacity 0.2s ease; }
  .qa-pane[hidden] { display: none; }
  /* Only while animating are both panes stacked/absolute -- at rest the
     live pane is in normal flow so the sheet sizes itself. */
  .qa-panes.is-animating .qa-pane { position: absolute; top: 0; left: 0; right: 0; }
  .qa-pane.is-pre-right { transform: translateX(100%); opacity: 0; }
  .qa-pane.is-pre-left  { transform: translateX(-100%); opacity: 0; }
  .qa-pane.is-out-left  { transform: translateX(-26%); opacity: 0; }
  .qa-pane.is-out-right { transform: translateX(26%); opacity: 0; }

  /* The "More options" row: deliberately quiet. A text row, not a tile, so
     it never competes with the actions above it -- but still a full 44px-tall
     target. The chevron points RIGHT because it opens a page. */
  .qa-more-link {
    width: 100%; margin-top: 10px;
    display: flex; align-items: center; justify-content: center; gap: 5px;
    min-height: 44px; padding: 12px;
    border: none; background: none; color: var(--text-secondary);
    font-family: inherit; font-size: 12.5px; font-weight: 600;
    letter-spacing: -0.005em; border-radius: 12px; cursor: pointer;
    opacity: 0.85;
    transition: background 0.16s ease, color 0.16s ease, opacity 0.16s ease;
  }
  .qa-more-link:hover { background: var(--sidebar-active); color: var(--text); opacity: 1; }
  .qa-more-link svg { flex-shrink: 0; }

  .qa-pane-head { display: flex; align-items: center; gap: 8px; margin: 2px 0 12px; }
  .qa-pane-title { font-size: 18px; font-weight: 800; letter-spacing: -0.015em; color: var(--text); }

  .qa-cancel {
    width: 100%; margin-top: 12px; padding: 15px;
    border-radius: 18px; border: 1.5px solid var(--border);
    background: none; color: var(--text); font-size: 15px; font-weight: 700;
    font-family: inherit; cursor: pointer;
    transition: border-color 0.15s ease, transform 0.1s ease;
  }
  .qa-cancel:hover { border-color: var(--text-secondary); }
  .qa-cancel:active { transform: scale(0.98); }

  /* Staggered pop-in every time the sheet opens (the "is-open" class is
     added by wireSheet). backwards fill holds each tile hidden until its
     delay, so they cascade up one after another. */
  .mt-sheet-overlay.is-open .qa-tile { animation: qa-pop 0.34s cubic-bezier(0.22, 1, 0.36, 1) backwards; }
  .mt-sheet-overlay.is-open .qa-tile:nth-child(1) { animation-delay: 0.05s; }
  .mt-sheet-overlay.is-open .qa-tile:nth-child(2) { animation-delay: 0.10s; }
  .mt-sheet-overlay.is-open .qa-tile:nth-child(3) { animation-delay: 0.15s; }
  .mt-sheet-overlay.is-open .qa-tile:nth-child(4) { animation-delay: 0.20s; }
  .mt-sheet-overlay.is-open .qa-tile:nth-child(5) { animation-delay: 0.25s; }
  .mt-sheet-overlay.is-open .qa-more-link { animation: qa-pop 0.34s cubic-bezier(0.22, 1, 0.36, 1) 0.30s backwards; }
  .mt-sheet-overlay.is-open .qa-cancel { animation: qa-pop 0.34s cubic-bezier(0.22, 1, 0.36, 1) 0.35s backwards; }
  @keyframes qa-pop {
    from { opacity: 0; transform: translateY(14px) scale(0.94); }
    to { opacity: 1; transform: none; }
  }
}

@media (max-width: 480px) {
  /* Pre-existing bug, independent of the safe-area fix above: this rule and
     the mobile `.app` rule (max-width:720px) both match on every real
     iPhone in portrait -- the narrowest is 375px, the widest Pro Max is
     430px, all comfortably under 480px. Same specificity, later source
     order wins, so this flat `padding: 8px` was silently overwriting BOTH
     the 82px floating-tab-bar clearance the mobile rule's own comment says
     is required (verified live: a bare reproduction of these two rules at
     390px computes padding-bottom: 8px, not the intended calc(82px + ...))
     and the safe-area-inset-top clearance just added above -- on every
     phone, not an edge case. Longhand-only here so this breakpoint keeps
     doing what it evidently meant to do (tighten the horizontal edges to
     8px on the narrowest phones) without silently reopening either of
     those two invariants, which live in a different rule and would give no
     visible signal here if broken again. */
  .app {
    padding-left: 8px;
    padding-right: 8px;
    padding-top: max(8px, env(safe-area-inset-top, 0px));
  }
  .card { padding: 14px; border-radius: var(--radius-md); }
  .topbar h1 { font-size: 19px; }
  .mobile-tabbar { left: 8px; right: 8px; gap: 8px; }
  .mt-item { padding: 12px 1px; }
  .mt-fab { width: 54px; height: 54px; }
}

/* ---------- The tab bar stops growing once the window is wider than a phone
   ----------
   The tab bar does NOT grow with the box. Stretched wide, its five items sit
   a thumb's width apart with the active bubble blown up into a blob --
   sparse, and nothing like the dense pill the design is.

   This used to live inside the `min-width: 721px` block, which left the whole
   481-720px band uncovered -- and that band is exactly where an iPad sits in
   Split View. Measured before this moved: at a 720px viewport the bar was
   696px wide, then snapped to 420px at 721px. Dragging a split-view divider
   across that boundary jumped the bar by 276px, and everywhere below it the
   five destinations were spread across the full width of a tablet, which is
   the failure this rule was written to prevent in the first place.

   481px rather than 0: at or below 480px the phone rule's tighter 8px
   gutters win, and a real phone should keep them -- max() would otherwise
   start capping from 444px up and quietly restyle the primary platform.

   That boundary is a WIDTH, not a device, so a phone in landscape does land
   in this band and does get the centred bar: an SE on its side is 667pt.
   That is the right outcome for the same reason it is right on a tablet --
   five destinations stretched across a landscape phone are no easier to
   reach -- but "phones are untouched" is only true in portrait, and the next
   person reading this should know which one it is. (Bigger phones in
   landscape are past 721 and take the tablet box outright: an 8 Plus is
   736pt, which is NOT this band.)

   The two breakpoints are a PAIR: this must start exactly one pixel above
   where the phone rule stops, or you get a gap (the bug this fixed) or an
   overlap (the cap stripping the phone's 8px gutters, since both rules are
   `.mobile-tabbar` at the same specificity). Moving one without the other is
   caught by tests/test_ipad_layout.py.

   This block must also stay AFTER the base `.mobile-tabbar` rule in the
   `@media all` block. Same selector, same specificity, no media query adds
   any -- so the cap wins on source order alone, and moving it earlier in the
   file makes it silently inert at every width.

   Insets rather than a transform, deliberately: `transform` on this element
   already belongs to the sheet-open recede (html.pc-sheet-active), and
   centring with translateX(-50%) would be silently cancelled the moment a
   sheet opened.

   `100%` on a fixed element resolves against its containing block: <body>
   above 721px, where the transform makes the box the containing block, and
   the viewport below it. Both are the width the bar should be centred in, so
   one rule is right in all three skins -- phone, split view, tablet column --
   and in the 390px desk frame the calc goes negative and max() hands back
   the phone's own 12px. */
@media (min-width: 481px) {
  .mobile-tabbar {
    left: max(12px, calc((100% - 420px) / 2));
    right: max(12px, calc((100% - 420px) / 2));
  }
}

/* Respect users who ask for less motion: drop the quick-actions stagger
   (the sheet still opens, just without the flourish). */
@media (prefers-reduced-motion: reduce) {
  .mt-sheet-overlay.is-open .qa-tile,
  .mt-sheet-overlay.is-open .qa-more-link,
  .mt-sheet-overlay.is-open .qa-cancel { animation: none; }
  .qa-panes, .qa-pane { transition: none; }
}

/* ---------- Tab-to-tab motion (static/pagenav.js) -------------------------
   Tapping a tab no longer loads a document -- pagenav.js replaces the
   contents of <main> in place -- so the transition between screens is ours
   to draw rather than the browser's. The ARRIVING screen slides in from the
   side it came from in the tab order: tapping rightwards brings the next
   screen in from the right, the way a row of pages behaves.

   Only the arriving screen is animated. The one you are leaving is never
   faded, and that is a correctness rule rather than a taste one: iOS
   snapshots the page when a navigation commits and replays that snapshot
   during the back-swipe, so a screen that was faded out at commit time is
   stored blank -- and swiping back to it slid a blank screen in, which read
   as the app reloading. Nothing to fade means nothing blank to store.

   Small distance on purpose (14px). This runs on every tab tap, many times a
   session, and motion that reads as elegant once reads as slow the twentieth
   time.

   The enter classes carry `transition: none` so the start position is not
   itself animated -- pagenav.js sets them, lets a frame paint, then removes
   them, and THAT is what animates.

   No `will-change` here, deliberately: it would make .main a containing
   block for position:fixed descendants permanently, quietly re-anchoring any
   fixed element a page renders inside <main>. The transform does that too,
   but only for the ~160ms a transition is actually running. */
.main {
  transition: opacity 0.16s ease, transform 0.16s ease;
}
.main.pn-enter-left,
.main.pn-enter-right { opacity: 0; transition: none; }
.main.pn-enter-left { transform: translateX(-14px); }
.main.pn-enter-right { transform: translateX(14px); }

/* Asked for less motion: no slide, no fade, just the new screen. pagenav.js
   also skips the classes entirely in that case -- this is the belt to that
   pair of braces, for a preference that changes mid-session. */
@media (prefers-reduced-motion: reduce) {
  .main,
  .main.pn-enter-left,
  .main.pn-enter-right {
    transition: none;
    transform: none;
    opacity: 1;
  }
}

/* ---------- Ultra-smooth page-to-page nav (View Transitions API) ----------
   On a real browser navigation the active bottom-bar "bubble" morphs from
   its old slot straight to the new page's slot, so moving between pages
   feels like one bubble gliding to the next instead of a hard reload. The
   page contents cross-fade underneath; the pill and "+" are named (above)
   so they stay rock-steady. Browsers without cross-document view
   transitions just navigate normally -- no animation, nothing breaks. */
@view-transition { navigation: auto; }

/* Quick, soft cross-fade of the page body between routes. */
::view-transition-old(root),
::view-transition-new(root) {
  animation-duration: 0.24s;
  animation-timing-function: ease;
}

/* The bubble glide: a touch of overshoot at the end so it settles with a
   gentle, "bubbly" spring rather than a mechanical stop. Outlasts the body
   cross-fade slightly so the bubble stays the focal point of the motion. */
::view-transition-group(mt-active-bubble) {
  animation-duration: 0.44s;
  animation-timing-function: cubic-bezier(0.34, 1.24, 0.64, 1);
}

/* Never animate for users who asked for reduced motion -- navigate flat. */
@media (prefers-reduced-motion: reduce) {
  ::view-transition-group(*),
  ::view-transition-old(*),
  ::view-transition-new(*) { animation: none !important; }
}

/* ---------- Shared logging bottom sheet (Workouts + Nutrition) ----------
   One Apple-style sheet used by both "log" flows, presented like the "+"
   quick-actions sheet: a grabber handle, a search field that settles in
   first, an X to dismiss, then the suggested + recent lists below. Slides
   up from the bottom on phones; centers as a card on wider screens.
   Toggled by the .is-open class from JS so the slide can animate. No own
   backdrop scrim -- the page behind (.app) dims and recedes on its own,
   see the ".pc-sheet-active .app" rule above, same as every other sheet. */
.log-sheet-overlay {
  position: fixed;
  left: 0;
  right: 0;
  /* top/height (not `inset: 0`) so this stays pinned to the real visible
     viewport -- see base.html's visualViewport tracking -- instead of the
     full layout viewport, which on mobile stays un-shrunk behind an open
     keyboard and would otherwise push a bottom-anchored sheet's pinned-top
     search box up out of view as the keyboard opens. */
  top: var(--pc-vvt, 0px);
  height: var(--pc-vvh, 100vh);
  z-index: 400;
  display: flex;
  align-items: flex-end;
  justify-content: center;
  opacity: 0;
  pointer-events: none;
  transition: opacity 0.46s ease;
}
.log-sheet-overlay.is-open,
.log-sheet-overlay.is-in { opacity: 1; pointer-events: auto; }

.log-sheet {
  pointer-events: auto;
  width: 100%;
  max-width: 560px;
  /* Capped against the tracked visible viewport (falls back to 92vh
     when unsupported), not a bare 92vh -- otherwise, with the keyboard
     open and the overlay above now sized to the shrunk visible area, the
     sheet could still want to be taller than what's actually on screen
     and push its own pinned-top search box off the top edge. */
  max-height: calc(var(--pc-vvh, 100vh) * 0.92);
  display: flex;
  flex-direction: column;
  background: var(--card-bg);
  color: var(--text);
  border-radius: 34px 34px 0 0;
  box-shadow: 0 -14px 48px rgba(0, 0, 0, 0.42);
  transform: translateY(100%);
  transition: transform 0.48s cubic-bezier(0.32, 0.72, 0, 1);
  overflow: hidden;
}
.log-sheet-overlay.is-open .log-sheet,
.log-sheet-overlay.is-in .log-sheet { transform: translateY(0); }

.log-sheet-handle {
  width: 38px; height: 5px;
  border-radius: 999px;
  background: var(--border);
  margin: 9px auto 3px;
  flex-shrink: 0;
  cursor: grab;
}
.log-sheet-handle:active { cursor: grabbing; }

.log-sheet-head {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 8px 16px 14px;
  flex-shrink: 0;
}
.log-sheet-search {
  flex: 1;
  min-width: 0;
  display: flex;
  align-items: center;
  gap: 10px;
  background: var(--bg);
  border: 1px solid var(--border);
  border-radius: 999px;
  padding: 12px 16px;
  color: var(--text-secondary);
  transition: border-color 0.16s ease, box-shadow 0.16s ease;
}
.log-sheet-search:focus-within {
  border-color: var(--blue);
  box-shadow: 0 0 0 3px var(--blue-bg);
  color: var(--blue);
}
.log-sheet-search svg { flex-shrink: 0; }
.log-sheet-search input {
  flex: 1;
  min-width: 0;
  background: none;
  border: none;
  outline: none;
  color: var(--text);
  font-size: 16px;
  font-family: inherit;
}
.log-sheet-search input::placeholder { color: var(--text-secondary); }
.log-sheet-close {
  flex-shrink: 0;
  width: 44px; height: 44px;
  border-radius: 50%;
  border: none;
  background: var(--bg);
  color: var(--text-secondary);
  font-size: 20px;
  line-height: 1;
  cursor: pointer;
  display: flex;
  align-items: center;
  justify-content: center;
  transition: background-color 0.15s ease, color 0.15s ease, transform 0.1s ease;
}
.log-sheet-close:hover { background: var(--sidebar-active); color: var(--text); }
.log-sheet-close:active { transform: scale(0.9); }

.log-sheet-body {
  flex: 1;
  min-height: 0;
  overflow-y: auto;
  -webkit-overflow-scrolling: touch;
  overscroll-behavior: contain;
  padding: 2px 16px calc(20px + env(safe-area-inset-bottom, 0px));
}

.log-sheet-overlay.is-entering .log-sheet-head {
  animation: log-sheet-rise 0.4s cubic-bezier(0.22, 1, 0.36, 1) 0.06s backwards;
}
.log-sheet-overlay.is-entering .log-sheet-body {
  animation: log-sheet-rise 0.46s cubic-bezier(0.22, 1, 0.36, 1) 0.18s backwards;
}
@keyframes log-sheet-rise {
  from { opacity: 0; transform: translateY(12px); }
  to { opacity: 1; transform: none; }
}

@media (prefers-reduced-motion: reduce) {
  .log-sheet { transition: none; }
  .log-sheet-overlay.is-entering .log-sheet-head,
  .log-sheet-overlay.is-entering .log-sheet-body { animation: none; }
}

/* ---------- Exercise picker: scope segments + favorite heart ----------
   Shared by the workouts log sheet and the analyze page's picker, which
   both show one filtered list under an iOS-style segmented control. Lives
   here rather than inline in either template so the two can't drift. */
.ex-scope-seg {
  display: flex; gap: 2px;
  background: var(--sidebar-active);
  border-radius: 9px;
  padding: 2px;
  margin: 0 0 12px;
}
.ex-scope-seg button {
  flex: 1;
  padding: 6px 4px;
  border: none;
  border-radius: 7px;
  background: none;
  color: var(--text-secondary);
  font-size: 13px;
  font-weight: 500;
  font-family: inherit;
  cursor: pointer;
}
.ex-scope-seg button.is-active {
  background: var(--card-bg);
  color: var(--text);
  font-weight: 700;
  box-shadow: var(--shadow);
}
.ex-modal-result-item span:nth-child(2) { flex: 1; }
.ex-modal-result-fav {
  background: none;
  border: none;
  color: var(--text-secondary);
  opacity: 0.5;
  cursor: pointer;
  padding: 4px;
  display: flex;
  align-items: center;
  flex-shrink: 0;
}
.ex-modal-result-fav:hover { opacity: 1; color: var(--red); }
.ex-modal-result-fav:active { transform: scale(0.85); }
.ex-modal-result-fav.is-fav { color: var(--red); opacity: 1; }

/* ---------- "Log weight" quick-action popup (base.html) ---------- */
.lw-modal-overlay {
  position: fixed;
  inset: 0;
  display: flex;
  align-items: flex-end;
  justify-content: center;
  z-index: 400;
  opacity: 0;
  pointer-events: none;
  transition: opacity 0.46s ease;
}
.lw-modal-overlay.is-open,
.lw-modal-overlay.is-in { opacity: 1; pointer-events: auto; }
.lw-modal {
  pointer-events: auto;
  background: var(--card-bg);
  border-radius: 34px 34px 0 0;
  box-shadow: 0 -14px 48px rgba(0, 0, 0, 0.42);
  width: 100%;
  max-width: 560px;
  max-height: calc(var(--app-h) * 0.92);
  transform: translateY(100%);
  transition: transform 0.48s cubic-bezier(0.32, 0.72, 0, 1);
  overflow: hidden;
  display: flex;
  flex-direction: column;
}
.lw-modal-overlay.is-open .lw-modal,
.lw-modal-overlay.is-in .lw-modal { transform: translateY(0); }
.lw-modal-handle {
  width: 38px; height: 5px;
  border-radius: 999px;
  background: var(--border);
  margin: 9px auto 2px;
  flex-shrink: 0;
  cursor: grab;
}
.lw-modal-handle:active { cursor: grabbing; }
.lw-modal-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 12px 20px 10px;
  flex-shrink: 0;
}
.lw-modal-title { font-size: 16px; font-weight: 700; color: var(--text); }
.lw-modal-close {
  width: 44px; height: 44px;
  display: flex; align-items: center; justify-content: center; flex-shrink: 0;
  background: none; border: none; font-size: 20px; line-height: 1;
  color: var(--text-secondary); cursor: pointer;
}
.lw-modal-close:hover { color: var(--text); }
.lw-modal-body { padding: 4px 20px 24px; flex: 1; overflow-y: auto; }
.lw-weight-input {
  width: 100%;
  padding: 12px 14px;
  border: 1px solid var(--border);
  background: var(--bg);
  color: var(--text);
  border-radius: 10px;
  font-size: 20px;
  font-weight: 700;
  text-align: center;
  font-family: inherit;
  margin-bottom: 12px;
  box-sizing: border-box;
}
.lw-save-btn {
  width: 100%;
  min-height: 44px; box-sizing: border-box;
  padding: 12px;
  border-radius: 999px;
  border: none;
  background: var(--text);
  color: var(--bg);
  font-size: 14px;
  font-weight: 700;
  cursor: pointer;
  font-family: inherit;
}
.lw-save-btn:disabled { opacity: 0.5; cursor: not-allowed; }
.lw-modal-msg { text-align: center; font-size: 12px; color: var(--text-secondary); margin-top: 10px; min-height: 15px; }
.lw-modal-msg.is-error { color: var(--red); }

/* Recent weigh-ins + link to the full history page -- see base.html's
   renderRecent(): this sheet became the only home-screen entry point for
   weight, so it's no longer just a bare "type today's number" popup.
   A trend sparkline (matching home.html's card) up top, then each row as
   its own soft tile with a colored day-over-day delta badge -- not a
   plain divided list. */
.lw-recent { margin-top: 20px; }
.lw-recent-title {
  font-size: 11px; font-weight: 800; color: var(--text-secondary);
  text-transform: uppercase; letter-spacing: 0.04em; margin: 0 2px 10px;
}
.lw-recent-spark { width: 100%; height: 30px; margin-bottom: 14px; }
.lw-recent-spark svg { width: 100%; height: 100%; display: block; }
.lw-recent-list { display: flex; flex-direction: column; gap: 7px; }
.lw-recent-row {
  display: flex; align-items: center; justify-content: space-between; gap: 10px;
  padding: 11px 14px; border-radius: 14px; background: var(--bg);
}
.lw-recent-date { font-size: 13.5px; font-weight: 600; color: var(--text-secondary); }
.lw-recent-right { display: flex; align-items: center; gap: 8px; }
.lw-recent-delta {
  font-size: 11px; font-weight: 800; padding: 3px 8px; border-radius: 999px;
  white-space: nowrap;
}
.lw-recent-delta.is-down { color: var(--green); background: var(--green-bg); }
.lw-recent-delta.is-up { color: var(--red); background: var(--red-bg); }
.lw-recent-value { font-size: 15px; font-weight: 800; color: var(--text); min-width: 50px; text-align: right; }
.lw-history-link {
  display: flex; align-items: center; justify-content: center; gap: 4px;
  min-height: 44px; margin-top: 10px;
  color: var(--blue); font-size: 14px; font-weight: 700; text-decoration: none;
}
.lw-history-link:active { opacity: 0.7; }

/* ---------- "Goal weight achieved" celebration sheet (base.html) ---------- */
.ga-modal { max-width: 480px; }
.ga-hero { text-align: center; padding: 6px 8px 22px; }
.ga-hero-icon {
  font-size: 44px; line-height: 1; margin-bottom: 10px;
  animation: ga-pop 0.55s cubic-bezier(0.22, 1, 0.36, 1) 0.05s backwards;
}
.ga-hero-title { font-size: 20px; font-weight: 800; color: var(--text); margin-bottom: 6px; }
.ga-hero-sub { font-size: 14.5px; color: var(--text-secondary); line-height: 1.4; }
@keyframes ga-pop {
  0% { opacity: 0; transform: scale(0.4); }
  70% { opacity: 1; transform: scale(1.15); }
  100% { opacity: 1; transform: scale(1); }
}
.ga-callout {
  background: var(--green-bg);
  border: 1px solid rgba(31, 169, 113, 0.28);
  border-radius: 16px;
  padding: 16px 18px;
  margin-bottom: 12px;
}
.ga-callout-title { font-size: 15.5px; font-weight: 800; color: var(--green); margin-bottom: 5px; }
.ga-callout-body { font-size: 13.5px; color: var(--text); opacity: 0.9; line-height: 1.4; margin-bottom: 14px; }
.ga-btn-primary {
  width: 100%;
  min-height: 46px; box-sizing: border-box;
  padding: 12px;
  border-radius: 999px;
  border: none;
  background: linear-gradient(135deg, var(--green), #29b87e);
  color: #fff;
  font-size: 15px;
  font-weight: 800;
  cursor: pointer;
  font-family: inherit;
}
.ga-btn-primary:active { opacity: 0.85; }
.ga-btn-secondary {
  width: 100%;
  min-height: 44px; box-sizing: border-box;
  padding: 12px;
  border-radius: 999px;
  border: none;
  background: none;
  color: var(--text-secondary);
  font-size: 14px;
  font-weight: 700;
  cursor: pointer;
  font-family: inherit;
}
.ga-btn-secondary:active { opacity: 0.7; }
@media (prefers-reduced-motion: reduce) {
  .ga-hero-icon { animation: none; }
}

/* ================================================================
   Analyze result: video-led layout
   ================================================================
   The results screen used to be a stack of white cards on the cool
   grey --bg, which read as chips on a circuit board once the tutorial
   card, hero card and breakdown card were all on screen at once. This
   replaces that with one surface: a frame of the lifter's own clip
   fills the top, and a warm paper sheet slides up over it carrying the
   score and the feedback.

   Everything here is scoped to html.an-result so no other page is
   touched. result.html sets it server-side; index.html's AJAX view
   toggles it when the result swaps in -- that page never navigates, so
   a page-level rule alone would either strand the class on the upload
   view or never apply at all. */

:root {
  --paper: #f7f6f4;
  --paper-line: #e6e3dd;
  --paper-dim: #9a978f;
}
:root[data-theme="dark"] {
  /* Warm near-black, deliberately not --bg's #0b0b0c: a pure cold black
     puts back exactly the sterile feel this layout exists to remove. */
  --paper: #141312;
  --paper-line: #2a2724;
  --paper-dim: #8b8781;
}

html.an-result, html.an-result body { background: var(--paper); }
/* Full bleed: the stage has to reach the screen edges, and the app
   shell's padding/gap/sidebar all fight that. */
html.an-result .app { padding: 0; gap: 0; display: block; overflow-x: clip; }
html.an-result .sidebar { display: none; }
html.an-result .main { max-width: none; padding-bottom: 0; }
/* The page's own topbar is replaced by .an-bar, which sits over the video. */
html.an-result .topbar { display: none; }
/* An analysis is an active task, not a destination -- same reasoning as
   the html.pc-sheet-active rule above, which already hides the tab bar
   while a sheet is open. Exits are .an-back and the "Analyze another
   video" row, both of which land on /analyze where the bar lives. */
html.an-result .mobile-tabbar { display: none; }

.an-wrap { position: relative; max-width: 520px; margin: 0 auto; }

/* ---------- Stage: the lifter's own clip ---------- */
/* position:sticky (not fixed) gives the parallax -- the sheet scrolls up
   over a stage that stays put -- without taking the stage out of flow, so
   the wrap still sizes normally and the max-width centering keeps working
   on desktop. */
.an-stage {
  position: sticky; top: 0; z-index: 0;
  height: calc(var(--app-h) * 0.56); min-height: 260px;
  overflow: hidden;
  background: #14171e;
}
/* With no clip there is nothing to look at, so the stage stops being the
   point and shrinks to a header band -- 56vh of empty gradient just pushes
   the score below the fold for no gain. Tall enough to still carry the
   back chevron and title. */
.an-stage.is-empty { height: calc(var(--app-h) * 0.24); min-height: 128px; }
.an-stage-media, .an-stage-media video {
  position: absolute; inset: 0;
  width: 100%; height: 100%;
  object-fit: cover;
  display: block;
}
/* No clip on disk (anonymous analyses never store one): a calm neutral
   field rather than a broken-looking black box. */
.an-stage-empty {
  position: absolute; inset: 0;
  background: radial-gradient(120% 90% at 30% 15%, #3d4657 0%, #232833 55%, #14171e 100%);
}
/* Four stops, not two: dark at the top so the back chevron and title stay
   legible over any footage, clear through the middle so the lift is
   actually visible, then paper at the bottom so the sheet edge dissolves
   into the video instead of cutting a hard line across it. */
.an-scrim {
  position: absolute; inset: 0;
  background: linear-gradient(180deg,
    rgba(10, 12, 16, 0.55) 0%,
    rgba(10, 12, 16, 0) 30%,
    rgba(10, 12, 16, 0) 58%,
    var(--paper) 100%);
  pointer-events: none;
}
/* ---------- Bar over the video ---------- */
/* Fixed, not absolute: the way out must not scroll away. */
.an-bar {
  position: fixed; top: 0; left: 0; right: 0; z-index: 30;
  max-width: 520px; margin: 0 auto;
  display: flex; align-items: center; justify-content: space-between; gap: 10px;
  padding: calc(12px + env(safe-area-inset-top, 0px)) 14px 12px;
  pointer-events: none;
}
.an-bar > * { pointer-events: auto; }
.an-bar-title {
  font-size: 15px; font-weight: 700; color: #fff; margin: 0;
  text-shadow: 0 1px 10px rgba(0, 0, 0, 0.5);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
  transition: color 0.2s ease, text-shadow 0.2s ease;
}

/* Scrolled past the video, the bar is over paper, where white-on-white is
   invisible and the glass circles have nothing to sit against. .is-stuck
   (toggled by the scroll handler in result.html / index.html) turns it
   into a normal opaque header. Colours transition so it reads as one bar
   changing state rather than a second bar appearing. */
.an-bar.is-stuck {
  background: color-mix(in srgb, var(--paper) 92%, transparent);
  -webkit-backdrop-filter: blur(14px); backdrop-filter: blur(14px);
  border-bottom: 1px solid var(--paper-line);
}
.an-bar.is-stuck .an-bar-title { color: var(--text); text-shadow: none; }
.an-bar.is-stuck .an-glass {
  background: color-mix(in srgb, var(--text) 7%, transparent);
  border-color: transparent;
  color: var(--text);
}
.an-glass {
  width: 34px; height: 34px; flex-shrink: 0;
  border-radius: 50%; border: 1px solid rgba(255, 255, 255, 0.2);
  background: rgba(255, 255, 255, 0.18);
  -webkit-backdrop-filter: blur(14px); backdrop-filter: blur(14px);
  display: flex; align-items: center; justify-content: center;
  color: #fff; font-size: 12px; font-weight: 700; text-decoration: none;
  cursor: pointer; padding: 0;
  transition: transform 0.1s ease, background-color 0.2s ease;
}
.an-glass svg { width: 17px; height: 17px; }
.an-glass:active { transform: scale(0.92); }
/* Balances the back chevron so the title stays optically centred, without
   drawing an empty circle where nothing is tappable. */
.an-glass-spacer { visibility: hidden; }

/* ---------- Sheet ---------- */
.an-sheet {
  position: relative; z-index: 1;
  margin-top: calc(var(--app-h) * -0.08);               /* overlap the stage: one surface, not two boxes */
  min-height: calc(var(--app-h) * 0.52);
  background: var(--paper);
  border-radius: 26px 26px 0 0;
  /* Bottom clearance has to outrun the fixed ask bar (~70px including its
     own padding and safe area) so the primary button is never tucked
     under it. */
  padding: 18px 22px calc(116px + env(safe-area-inset-bottom, 0px));
}
.an-grabber {
  width: 36px; height: 4px; border-radius: 999px;
  background: var(--paper-line);
  margin: 0 auto 18px;
}
.an-score {
  font-size: 56px; font-weight: 800; letter-spacing: -0.04em; line-height: 0.9;
  color: var(--text);
}
.an-score.is-good { color: var(--green); }
.an-score.is-ok { color: var(--amber); }
.an-score.is-bad { color: var(--red); }
.an-score-sub {
  font-size: 10px; font-weight: 700; color: var(--paper-dim);
  text-transform: uppercase; letter-spacing: 0.06em; margin-top: 6px;
}
.an-verdict {
  font-size: 14.5px; font-weight: 600; line-height: 1.5;
  color: var(--text); margin-top: 15px;
}
/* Rep count. Gemini counts the reps on every analysis, but the number used
   to reach the user only buried inside the Movement Summary prose -- so the
   one figure a lifter tracks set to set was the one thing they had to read
   a paragraph to find. A chip directly under the grade gives it a home
   without competing with the 56px score: inline-flex so it hugs its own
   content rather than stretching to the sheet width, and baseline-aligned
   so the big number and its label sit on one line. */
.an-reps {
  display: inline-flex; align-items: baseline; gap: 7px;
  margin-top: 13px; padding: 7px 13px;
  border-radius: 999px;
  background: var(--purple-bg); color: var(--purple);
}
.an-reps-value {
  font-size: 19px; font-weight: 800; letter-spacing: -0.02em; line-height: 1;
}
.an-reps-label {
  font-size: 10px; font-weight: 700;
  text-transform: uppercase; letter-spacing: 0.06em;
}

.an-note {
  margin-top: 14px; padding: 11px 14px; border-radius: var(--radius-md);
  background: var(--blue-bg); color: var(--blue);
  font-size: 13.5px; font-weight: 500;
}
.an-note strong { text-transform: capitalize; }

/* Feedback sections: one softly tinted card per category, so a glance says
   whether you're reading a win, a fix, or a safety warning before you've
   read a word of it. Only two of these are ever above the fold (positives +
   progress -- see FB_ALWAYS_VISIBLE); everything else sits behind "Full
   report", so the sheet never becomes a grid of six coloured boxes. */
.an-sec {
  margin-top: 12px;
  padding: 15px 16px;
  border-radius: 14px;
  background: var(--sidebar-active);
}
.an-sec-label {
  display: flex; align-items: center; gap: 7px;
  font-size: 13px; font-weight: 800; letter-spacing: -0.01em;
  margin-bottom: 8px;
}
/* app.py ships these icons at 19px; the label wants them smaller, and
   never squeezed by a long translated title. */
.an-sec-label svg { width: 15px; height: 15px; flex-shrink: 0; }
.an-sec-body { font-size: 14px; line-height: 1.55; color: var(--text); }
.an-sec-body p { margin: 0 0 8px; }
.an-sec-body p:last-child { margin-bottom: 0; }
.an-sec-body ul { margin: 8px 0 0; padding-left: 18px; }
.an-sec-body li { margin-bottom: 7px; }
.an-sec-body li:last-child { margin-bottom: 0; }
.an-sec-detail { margin-top: 10px; }
.an-sec-detail[hidden] { display: none; }

/* Each category's tint + label colour. Same accent-on-soft-tint pairing
   the rest of the app already uses (see .an-note above), so these read as
   part of the set rather than a new palette. The dark theme
   only re-tints the *-bg tokens, which keeps the pairing working there
   without a second set of rules. */
.an-sec-positives { background: var(--green-bg); }
.an-sec-positives .an-sec-label { color: var(--green); }
.an-sec-negatives { background: var(--red-bg); }
.an-sec-negatives .an-sec-label { color: var(--red); }
.an-sec-improvements { background: var(--blue-bg); }
.an-sec-improvements .an-sec-label { color: var(--blue); }
.an-sec-injury { background: var(--amber-bg); }
.an-sec-injury .an-sec-label { color: var(--amber); }
.an-sec-progress { background: var(--purple-bg); }
.an-sec-progress .an-sec-label { color: var(--purple); }
.an-sec-summary .an-sec-label { color: var(--text-secondary); }

/* Tap rows: "Full report", "Analyze another video". */
.an-row {
  display: flex; align-items: center; justify-content: space-between; gap: 12px;
  width: 100%; padding: 19px 0; margin: 0;
  border: none; border-top: 1px solid var(--paper-line); background: none;
  color: var(--text); font-family: inherit; font-size: 14px; font-weight: 700;
  text-align: left; text-decoration: none; cursor: pointer;
}
.an-row:active { opacity: 0.6; }
.an-row-chev { flex-shrink: 0; color: var(--paper-dim); transition: transform 0.24s ease; }
.an-row-chev svg { width: 16px; height: 16px; display: block; }
.an-row[aria-expanded="true"] .an-row-chev { transform: rotate(90deg); }
.an-more[hidden] { display: none; }

/* ---------- Sheet buttons ----------
   Fully rounded pills, matching .btn-primary's 999px radius so they read
   as the same control family as the rest of the app. Two weights: the
   solid blue one is the page's single primary action ("Analyze another
   video"), the outlined one is a helpful aside ("How to perform this
   exercise") that sits under the verdict and must not compete with it. */
.an-btn {
  display: flex; align-items: center; justify-content: center; gap: 8px;
  width: 100%; padding: 14px 18px; border: none; border-radius: 999px;
  font-family: inherit; font-size: 14px; font-weight: 700;
  text-align: center; text-decoration: none; cursor: pointer;
  transition: opacity 0.15s ease, transform 0.1s ease, box-shadow 0.15s ease,
              border-color 0.15s ease, background-color 0.15s ease;
}
.an-btn:active { transform: scale(0.98); }
.an-btn svg { width: 16px; height: 16px; flex-shrink: 0; }

.an-btn-primary {
  background: linear-gradient(135deg, #4d7ff5, var(--blue));
  box-shadow: 0 8px 20px rgba(47, 102, 232, 0.3);
  color: #fff;
  margin-top: 22px;
}
.an-btn-primary:hover { opacity: 0.92; box-shadow: 0 10px 24px rgba(47, 102, 232, 0.38); }

.an-btn-secondary {
  background: var(--card-bg);
  border: 1px solid var(--paper-line);
  color: var(--text);
  /* Generous on both sides: this button sits between the verdict above and
     the report below, and with only 18px of clearance the three of them
     read as one cramped block rather than three separate things. */
  margin-top: 26px;
  margin-bottom: 20px;
}
.an-btn-secondary:hover { border-color: var(--text-secondary); }
/* Same red play chip as the old tutorial card, so the link to "watch a
   demo" survives the move out of that card. */
.an-btn-secondary svg { color: #ff0000; }

@media (max-width: 380px) {
  .an-sheet { padding-left: 18px; padding-right: 18px; }
  .an-score { font-size: 48px; }
}

/* ---------- Ask bar: the AI, pinned to the bottom ----------
   Same #ac-* DOM and the same .is-open toggle the widget JS already
   drives -- only the position and shape change. Collapsed it is a full
   width input-looking pill (a bare sparkle circle reads as decoration,
   which is why the old dock needed a caption under it); open, the panel
   grows upward above it. */
html.an-result .ag-dock {
  position: fixed; z-index: 40;
  left: 0; right: 0; bottom: 0;
  max-width: 520px; margin: 0 auto;
  flex-direction: column-reverse;   /* panel grows UP from the bar */
  align-items: stretch;
  padding: 10px 14px calc(12px + env(safe-area-inset-bottom, 0px));
  background: linear-gradient(180deg, rgba(247, 246, 244, 0) 0%, var(--paper) 46%);
  transform: none;
}
html.an-result[data-theme="dark"] .ag-dock {
  background: linear-gradient(180deg, rgba(20, 19, 18, 0) 0%, var(--paper) 46%);
}
html.an-result .ag-dock.is-open {
  top: 0;
  background: color-mix(in srgb, var(--paper) 97%, transparent);
  -webkit-backdrop-filter: blur(22px); backdrop-filter: blur(22px);
  padding-top: calc(56px + env(safe-area-inset-top, 0px));
}
html.an-result .ag-bar {
  width: 100%; height: auto; min-height: 48px;
  border-radius: 999px;
  background: var(--card-bg);
  border: 1px solid var(--paper-line);
  box-shadow: 0 8px 24px rgba(0, 0, 0, 0.12);
  padding: 0 6px 0 14px;
  gap: 8px;
  animation: none;                  /* no breathing glow: it is already visible */
}
html.an-result .ag-dock.is-open .ag-bar { width: 100%; height: auto; }
/* The spark stops being the button and becomes the field's leading icon --
   the whole bar is the target now. */
html.an-result .ag-spark {
  width: 20px; height: 20px; color: var(--purple);
  pointer-events: none;
}
html.an-result .ag-spark svg { width: 18px; height: 18px; }
html.an-result .ag-dock.is-open .ag-spark { width: 20px; height: 20px; }
html.an-result .ag-dock.is-open .ag-spark svg { width: 18px; height: 18px; }
html.an-result .ag-inputrow { opacity: 1; padding-right: 0; min-height: 46px; }
html.an-result .ag-send { width: 34px; height: 34px; }
html.an-result .ag-send svg { width: 16px; height: 16px; }
/* The caption is redundant once the field itself says "Ask about your
   form" -- the placeholder does that job. */
html.an-result .ag-dock-label { display: none; }
html.an-result .ag-panel { max-width: none; }
html.an-result .ag-dock.is-open .ag-panel-inner { padding-top: 0; padding-bottom: 10px; }
html.an-result .ag-thread {
  max-height: none; flex: 1;
  background: none; border: none; box-shadow: none;
  -webkit-backdrop-filter: none; backdrop-filter: none;
  padding: 0 2px;
}
html.an-result .ag-suggest { justify-content: flex-start; }
html.an-result .ag-chip {
  background: var(--card-bg);
  border-color: var(--paper-line);
  -webkit-backdrop-filter: none; backdrop-filter: none;
}
html.an-result .ag-row-assistant .ag-bubble {
  background: color-mix(in srgb, var(--text) 7%, transparent);
  -webkit-backdrop-filter: none; backdrop-filter: none;
}
html.an-result .ag-row-user .ag-bubble {
  background: var(--purple);
  border-color: var(--purple);
  -webkit-backdrop-filter: none; backdrop-filter: none;
}
/* Close affordance, only meaningful once the panel is up. */
html.an-result .ag-close { display: none; }
html.an-result .ag-dock.is-open .ag-close {
  position: absolute; top: calc(14px + env(safe-area-inset-top, 0px)); right: 14px;
  display: flex; align-items: center; justify-content: center;
  width: 30px; height: 30px; border-radius: 50%; border: none; padding: 0;
  background: color-mix(in srgb, var(--text) 8%, transparent);
  color: var(--text-secondary); cursor: pointer;
}
html.an-result .ag-close svg { width: 13px; height: 13px; }

/* ================================================================
   Wide screens: one app box, two skins
   ================================================================
   RepCheck is a phone app. It used to grow a second, sidebar-based
   layout above 720px, which meant every screen had to work twice and the
   desktop version was always the neglected one. Instead the phone layout
   is the only layout (see the `@media all` block above) and a wide screen
   just puts it in a box.

   How that box works, because it is load-bearing:

   - <body> becomes the app: a fixed-size, non-scrolling box centred in
     the window. It carries `transform`, which makes it the containing
     block for every `position: fixed` descendant -- the tab bar, the "+"
     sheets, the analyze ask bar. Without that they would pin to the
     window and float outside the app entirely.
   - .app becomes the scroller. Because the fixed chrome hangs off <body>
     rather than .app, it stays pinned to the box while content scrolls
     underneath, exactly as it does on a real phone.
   - That moves the scrollport off the window, so JS that reads
     window.scrollY has to ask the scroller instead. base.html exposes
     RepCheck.scroller() / scrollTop() / scrollTo() for that, and the
     handful of call sites use them.

   The DEFAULT box is a tablet one: a comfortable centred column, full
   screen height, no decoration. Below it, ONE exception re-skins that box
   as a phone on a dark desk -- but only for a screen that is definitely a
   desktop with a mouse.

   That direction is deliberate, and it is the fix for a rejection. The
   desk used to be the default for every window over 720px, which is every
   iPad in both orientations (820pt portrait, 1180pt landscape). Apple
   rejected 0.7.1 (33) on an iPad Air under Guideline 4 for it: inside a
   native iPad app a drawn-on phone bezel is not a design flourish, it is
   the app failing to lay out for the device it is running on.

   So the exception has to prove itself with two independent signals --
   `hover: hover` AND `pointer: fine` -- and anything failing either one
   falls through to the tablet layout. A tablet reports `hover: none` and
   `pointer: coarse`, so both would have to be wrong at once for it to get
   the desk back. Written the other way round (tablet as the exception), a
   single wrong signal on some future device puts a fake phone in front of
   a reviewer again -- and that failure costs a whole review cycle to even
   find out about.

   Below 721px none of this applies: <body> scrolls, fixed means fixed,
   and the app is just the app. */


@media (min-width: 721px) {
  html {
    min-height: 100%;
    /* The app <body> below is position:absolute, so it still contributes
       its pre-transform box to the window's scroll area -- leaving ~700px of
       empty background you could scroll into under it. .app is the real
       scroller here (see below), so the window never needs to scroll. */
    overflow: hidden;
    /* The surround, not a contrasting "desk". The analyze result screen's
       own `html.an-result` paper background (higher specificity, see the
       an-result block above) still wins over this, so a full-bleed result
       page keeps its surface out to the screen edge. */
    background: var(--bg);
  }

  body {
    /* Full screen height: a tablet has no reason to letterbox itself.
       --app-h is what everything that used to be sized in vh reads. */
    --app-h: 100dvh;
    /* Wide enough to read at a tablet's viewing distance, narrow enough
       that phone-proportioned cards don't stretch into a letterbox. An
       iPad in portrait (820pt) is very nearly the full width; in landscape
       (1180pt) this column is centred with the app's own background either
       side, so the surround reads as part of the app rather than as a wall
       it has been propped against. */
    width: min(100vw, 720px);
    height: var(--app-h);
    margin: 0 auto;
    position: absolute;
    top: 50%; left: 50%;
    /* translate does the centring AND establishes the containing block
       for fixed descendants. Both jobs, one property. */
    transform: translate(-50%, -50%);
    overflow: hidden;
  }

  /* .app takes over scrolling from the window. */
  .app {
    height: 100%;
    min-height: 0;
    overflow-y: auto;
    overflow-x: hidden;
    -webkit-overflow-scrolling: touch;
    scrollbar-width: none;
    /* A little more gutter than a phone gets -- 12px of edge padding on a
       720px column reads as content jammed against the edge. The desk skin
       below puts it back to 12px, since inside a 390px frame the roomier
       gutter would only make the content narrower. */
    padding-left: 20px;
    padding-right: 20px;
  }
  .app::-webkit-scrollbar { display: none; }

  /* The tab bar cap lives in its own min-width: 481px block above, so it
     also covers the split-view band. Nothing to do here. */

  /* The auth pages (login, signup, onboarding) do not use .app, so they
     scroll on their own wrapper instead.

     .auth-body keeps auth.css's flex centring here. It used to be forced
     to `display: block` so .auth-wrap could take `height: 100%` and be the
     scroller -- but block layout also left the 400px card hard against the
     left edge of the box. Inside the 390px desktop frame that is invisible
     (the card is as wide as the frame), so it went unnoticed; in the
     tablet column it put the login screen visibly off to one side, above a
     screen and a half of empty space. `max-height` on the wrap gives the
     same overflow safety net without taking the centring away.

     Onboarding gets exactly the same treatment, and needs it more. <body>
     here is a fixed-height box with `overflow: hidden`, and so is <html>,
     so a page whose wrapper does not scroll has NO scrollport at all --
     content past the bottom edge is clipped and unreachable, with no
     gesture that can reach it. A combined onboarding step runs to about
     twice a phone viewport (see updateScrollCue in static/onboarding.js),
     and it is the first screen after signup, which is the first thing App
     Review reaches after the login page. */
  /* `html body.…` for specificity, not decoration: both pages set their own
     `min-height: 100vh` in a stylesheet that loads AFTER this one (auth.css
     for log in and sign up, an inline <style> for onboarding), so a plain
     `.auth-body` loses on source order and `body.ob-body` outranks it
     outright. Left unreleased, that min-height keeps the box at the LARGE
     viewport while `height` above is the dynamic one, so on a browser
     showing a URL bar the bottom of the card sits below the box's clipped
     edge -- the same unreachable-content bug, one step subtler. */
  html body.auth-body,
  html body.ob-body { height: 100%; min-height: 0; }
  .auth-wrap, .ob-wrap {
    max-height: 100%;
    overflow-y: auto;
    scrollbar-width: none;
  }
  .auth-wrap::-webkit-scrollbar,
  .ob-wrap::-webkit-scrollbar { display: none; }

  /* Analyze results paint their own full-bleed surface, so the device has
     to match or a paper-coloured sheet sits on a white body corner. */
  html.an-result body { background: var(--paper); }

  /* The sheet recede (html.pc-sheet-active) scales .app. Inside a device
     that is correct -- the screen contents recede, the phone does not --
     but the rounded corners have to come from the device, not the
     content, or you get a rounded rectangle inside a rounded rectangle. */
  html.pc-sheet-active .app { border-radius: 0; }
}

/* ---------- The exception: a desktop with a mouse ----------
   Re-skins the box above as a presentation device: a 390px phone, bezel
   and all, on a dark desk. Both `hover: hover` and `pointer: fine` are
   required -- see the note at the top of this section for why the test is
   deliberately hard to pass rather than easy. */
@media (min-width: 721px) and (hover: hover) and (pointer: fine) {
  html {
    /* The desk the phone sits on. Deliberately not pure grey -- a flat
       mid-grey behind a floating object reads as an unstyled default. */
    background:
      radial-gradient(120% 90% at 50% 0%, #2b2f3a 0%, #1b1e26 55%, #14161c 100%);
  }
  :root[data-theme="dark"] html,
  html[data-theme="dark"] {
    background:
      radial-gradient(120% 90% at 50% 0%, #23262e 0%, #15171c 55%, #101216 100%);
  }

  body {
    /* Capped at 844 (a standard phone) but allowed to shrink on a short
       laptop screen so the device is never taller than the window. */
    --app-h: min(844px, calc(100vh - 96px));
    width: 390px;
    border-radius: 46px;
    /* The bezel. A border rather than a wrapper element, so no page needs
       extra markup to sit inside the device. */
    border: 11px solid #0b0c0f;
    box-shadow:
      0 0 0 1px rgba(255, 255, 255, 0.06),
      0 40px 80px rgba(0, 0, 0, 0.55),
      0 8px 24px rgba(0, 0, 0, 0.4);
  }

  /* Back to the phone's own gutters: the column's roomier 20px would only
     make the content narrower inside a 390px frame. */
  .app { padding-left: 12px; padding-right: 12px; }
}

/* A short window would otherwise crop the device. Below this the phone
   stops being a fixed 844 and just fills what is available. Desk skin
   only: the tablet box is already full-height and has no bezel to shrink. */
@media (min-width: 721px) and (hover: hover) and (pointer: fine) and (max-height: 700px) {
  body { --app-h: calc(100vh - 32px); border-radius: 34px; border-width: 8px; }
}

/* ---------- Dimming the surround (tablet only) ----------
   A full-bleed overlay in this layout is `position: fixed` inside the
   transformed <body>, so it is clipped to the column and its dim stops at
   the column edge. On an iPad in landscape that leaves a bright 230px band
   down each side of a dimmed app, which reads as a rendering fault rather
   than a design. <body> cannot simply be widened -- its `overflow: hidden`
   is what gives the desk skin its bezel -- so the surround is painted to
   match instead.

   Deliberately NOT in the desk skin: there the dim SHOULD stop at the
   phone's edge, because there the surround is a desk rather than part of
   the app. Hence `pointer: coarse` rather than the shared block.

   Gated on that one signal alone, unlike the desk skin's two, and the
   asymmetry is the point: the worst case if this gate misses is the
   undimmed band staying, which is cosmetic. The desk gate guards against
   showing a fake phone, which is a rejection. Different stakes, different
   strictness.

   Both values are the exact composite of whatever is dimming the column:
   rgba(8, 10, 18, 0.45) for the tour's spotlight backdrop, and
   brightness(0.7) for the sheet recede, which multiplies toward black.
   <body> is painted for the sheet case as well -- .app scales to 0.92
   there, revealing body's own background as a ring inside the column. */
@media (min-width: 721px) and (pointer: coarse) {
  html.tour-dim {
    background: color-mix(in srgb, var(--bg) 55%, rgb(8, 10, 18));
  }
  html.pc-sheet-active,
  html.pc-sheet-active body {
    background: color-mix(in srgb, var(--bg) 70%, #000);
  }
}

/* ---------- Mascot empty states ----------
   The shared block behind every "there's nothing here yet" screen: the
   mascot drawing (see static/mascot.js), a short bold headline, and a
   quieter line under it. Used by the Hyrox leaderboard, both search
   sheets, and challenges, so all four read as one thing rather than four
   separately-invented empty states.

   Sizes match the .hx-history-empty-title / .wl-empty-title pair that
   already existed, so unifying on this doesn't change the two screens
   that were already doing it properly. */
.rc-empty { text-align: center; padding: 26px 0 18px; }
.rc-empty-art {
  width: 116px;
  height: auto;
  display: block;
  margin: 0 auto 14px;
}
.rc-empty-title { font-size: 15px; font-weight: 700; color: var(--text); line-height: 1.3; }
/* The searched-for term specifically -- it's what the eye goes looking
   for when a search misses, so it carries more weight than the sentence
   around it. */
.rc-empty-q { font-weight: 800; }
.rc-empty-sub { font-size: 13px; color: var(--text-secondary); margin-top: 5px; line-height: 1.5; }

/* Tighter inside the logging sheets, where the mascot shares a fixed
   sheet height with the search field and the results list above it. */
.log-sheet .rc-empty,
.split-modal .rc-empty { padding: 18px 0 10px; }
.log-sheet .rc-empty-art,
.split-modal .rc-empty-art { width: 104px; margin-bottom: 12px; }


/* Scheduled-account-deletion banner (base.html). Only ever rendered for an
   account that has asked to be deleted, so it is allowed to be loud. */
.deletion-banner {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  flex-wrap: wrap;
  margin: 0 0 16px;
  padding: 12px 16px;
  border-radius: 12px;
  background: var(--red-bg);
  color: var(--red);
  font-size: 14px;
  font-weight: 500;
}
.deletion-banner a { color: inherit; font-weight: 600; text-decoration: underline; white-space: nowrap; }

/* Privacy policy / terms (templates/privacy.html, templates/terms.html).
   Long-form prose, which nothing else in the app is -- hence its own
   measure and heading rhythm rather than reusing a card style. */
.policy-page {
  max-width: 62ch;
  background: var(--card-bg);
  border: 1px solid var(--border);
  border-radius: 16px;
  padding: 24px;
  line-height: 1.65;
}
.policy-page h2 {
  margin: 28px 0 8px;
  font-size: 17px;
  font-weight: 700;
}
.policy-page h2:first-of-type { margin-top: 20px; }
.policy-page p, .policy-page li { color: var(--text-secondary); font-size: 15px; }
.policy-page ul { margin: 8px 0 0; padding-left: 20px; }
.policy-page li { margin-bottom: 6px; }
.policy-page strong { color: var(--text); }
.policy-page a { color: var(--text); text-decoration: underline; }
.policy-updated { font-size: 13px; margin: 0 0 20px; }

/* /sources (templates/sources.html): the citation list behind the AI chats. */
.sources-list { list-style: none; padding-left: 0 !important; }
.sources-list li { margin-bottom: 12px; }
.sources-list a { display: block; overflow-wrap: anywhere; }
.sources-publisher { display: block; font-size: 13px; margin-top: 2px; }
.sources-footnote { margin-top: 28px; font-size: 14px; }

/* The "Sources" list under an AI chat reply (static/ai_sources.js). Shared
   by the coach page, the workout chat and the Analyze chat, so it sizes
   off the bubble it sits in rather than any one page's type scale. */
.ai-sources {
  margin-top: 10px;
  padding-top: 8px;
  border-top: 1px solid var(--border);
  font-size: 0.8em;
  line-height: 1.45;
}
.ai-sources-label {
  font-weight: 700;
  color: var(--text-secondary);
  text-transform: uppercase;
  letter-spacing: 0.04em;
  font-size: 0.85em;
  margin-bottom: 4px;
}
.ai-sources ol { margin: 0; padding-left: 1.3em; }
.ai-sources li { margin: 0 0 4px; }
.ai-sources a { color: var(--blue); text-decoration: underline; overflow-wrap: anywhere; }
.ai-sources-pub { display: block; color: var(--text-secondary); }
.ai-sources-all { display: inline-block; margin-top: 2px; color: var(--text-secondary); text-decoration: underline; }

/* "AI estimate, not medical advice" under the analysis result (1.4.1). */
.an-estimate-note { font-size: 12px; line-height: 1.5; color: var(--text-secondary); margin: 14px 2px; }

/* Caution under the weight-loss rate slider when it is above the standard
   zone (static/onboarding.js and static/coaching.js, Guideline 1.4). */
.rate-caution {
  margin: 10px 0 0;
  padding: 10px 12px;
  border-radius: 12px;
  background: var(--amber-bg, rgba(245, 158, 11, 0.12));
  color: var(--text);
  font-size: 12.5px;
  line-height: 1.45;
}

/* "AI-generated, not medical advice" in every AI chat reply's footer. */
.ai-disclaimer { margin-top: 8px; font-size: 0.75em; color: var(--text-secondary); }

/* Report control under an AI reply (static/ai_sources.js, Guideline 4.7.1). */
.ai-report { margin-top: 8px; font-size: 0.78em; line-height: 1.4; }
.ai-report-btn, .ai-report-reason, .ai-report-cancel {
  font: inherit;
  cursor: pointer;
  border-radius: 999px;
  min-height: 32px;
}
.ai-report-btn {
  display: inline-flex; align-items: center; gap: 5px;
  background: none; border: none; padding: 4px 0;
  color: var(--text-secondary);
}
.ai-report-btn:hover { color: var(--red, #d33); }
.ai-report-prompt { font-weight: 700; color: var(--text); margin-bottom: 6px; }
.ai-report-reasons { display: flex; flex-wrap: wrap; gap: 6px; }
.ai-report-reason, .ai-report-cancel {
  padding: 5px 12px;
  border: 1px solid var(--border);
  background: var(--card-bg);
  color: var(--text);
}
.ai-report-reason:hover { border-color: var(--red, #d33); color: var(--red, #d33); }
.ai-report-cancel { color: var(--text-secondary); }
.ai-report-done { color: var(--text-secondary); }

/* Consent to AI processing (static/ai_consent.js, Guideline 5.1.2(i)). A
   plain centred card rather than a bottom sheet: it is asked once, it must
   not be swiped away by accident, and it has to sit above any open sheet. */
.aic-overlay {
  position: fixed;
  inset: 0;
  z-index: 10000;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 16px;
  background: rgba(0, 0, 0, 0.45);
}
.aic-card {
  width: 100%;
  max-width: 420px;
  max-height: calc(100% - 32px);
  overflow-y: auto;
  background: var(--card-bg);
  color: var(--text);
  border-radius: 20px;
  padding: 22px 20px 18px;
  box-shadow: 0 20px 50px rgba(0, 0, 0, 0.3);
}
.aic-title { font-size: 19px; font-weight: 800; margin: 0 0 8px; }
.aic-body, .aic-fine { font-size: 14px; line-height: 1.5; color: var(--text-secondary); margin: 0 0 10px; }
.aic-list { margin: 0 0 12px; padding-left: 18px; font-size: 14px; line-height: 1.45; }
.aic-list li { margin-bottom: 5px; }
.aic-link { font-size: 13px; color: var(--blue); text-decoration: underline; }
.aic-error { color: var(--red, #d33); font-size: 13px; margin: 10px 0 0; }
.aic-actions { display: flex; gap: 10px; margin-top: 18px; }
.aic-btn {
  flex: 1;
  min-height: 46px;
  border-radius: 999px;
  font: inherit;
  font-size: 15px;
  font-weight: 700;
  cursor: pointer;
}
.aic-btn-secondary { background: none; border: 1px solid var(--border); color: var(--text); }
.aic-btn-primary { background: var(--blue); border: none; color: #fff; }
.aic-btn:disabled { opacity: 0.6; cursor: default; }
