/* Rindo's Fitness Hub — chart chrome per dataviz reference palette. */

:root {
  color-scheme: light;
  --page: #f9f9f7;
  --surface: #fcfcfb;
  --text-primary: #0b0b0b;
  --text-secondary: #52514e;
  --text-muted: #898781;
  --grid: #e1e0d9;
  --baseline: #c3c2b7;
  --border: rgba(11, 11, 11, 0.10);
  --silhouette: #efeeea;
  --band: rgba(11, 11, 11, 0.055);
  /* Status palette — fixed, never themed */
  --status-critical: #d03b3b;
  --status-under: #ec835a;
  --status-on: #0ca30c;
  /* "Past the goal" was amber until 2026-07-30. Rindo: amber "feels like it is
     a step below green, which is not the case" — past the goal is a different
     kind of thing, not a worse version of on-track, and he reads it as "dial
     this one back". Violet says that; it sits outside the red→green ramp
     instead of on it. Bright enough to hold up on both themes (the status
     palette is never themed) and it glows — his ask. */
  --status-above: #a855f7;
  --status-above-glow: rgba(168, 85, 247, 0.9);
  --status-none: #898781;
  /* Plain "something needs your attention" chrome. Used to borrow
     --status-above, which meant the banner turned violet the moment that
     became a status colour rather than a warning colour. */
  --warn: #fab219;
  /* Personal-best gold (2026-08-02): its own colour, off the status ramp. */
  --pb: #8a6400;
  /* Series blue for the BF% line (2026-08-12): categorical slot 1 of the
     dataviz reference palette, per mode. Not a status colour — a body-fat
     reading is a fact, not a verdict. */
  --series-1: #2a78d6;
}
@media (prefers-color-scheme: dark) {
  :root:where(:not([data-theme="light"])) {
    color-scheme: dark;
    --page: #0d0d0d;
    --surface: #1a1a19;
    --text-primary: #ffffff;
    --text-secondary: #c3c2b7;
    --text-muted: #898781;
    --grid: #2c2c2a;
    --baseline: #383835;
    --border: rgba(255, 255, 255, 0.10);
    --silhouette: #242423;
    --band: rgba(255, 255, 255, 0.07);
    --pb: #e8c35a;
    --series-1: #3987e5;
  }
}
:root[data-theme="dark"] {
  color-scheme: dark;
  --page: #0d0d0d;
  --surface: #1a1a19;
  --text-primary: #ffffff;
  --text-secondary: #c3c2b7;
  --text-muted: #898781;
  --grid: #2c2c2a;
  --baseline: #383835;
  --border: rgba(255, 255, 255, 0.10);
  --silhouette: #242423;
  --band: rgba(255, 255, 255, 0.07);
  --pb: #e8c35a;
  --series-1: #3987e5;
}

/* ---------- holiday mode chrome (Rindo, 2026-08-01) ---------- */
/* His ask: "a tropical-themed mode, but not too jarring, just enough for me
   to know that the app mode is for bodyweight exercises." So: a quiet shift
   of the SURFACES toward sand and sea-glass while `body.holiday` is set
   (render() keeps the class in sync with the 🏝 switch). The status palette
   is untouched on purpose — it is never themed (see :root above), and that
   includes holiday mode: red/green/violet must mean the same thing on a
   beach as at the gym. The verdict ramp, seg-late and --warn don't move
   either. Exact tints are this session's draft — tweak freely. */
body.holiday {
  --page: #f6f1e3;      /* warm sand instead of near-white */
  --surface: #fbf7ec;
  --grid: #e5dfcc;
  --baseline: #c8c0a6;
  --border: rgba(62, 48, 16, 0.12);
  --silhouette: #eee8d6;
  --band: rgba(13, 148, 136, 0.07); /* sea-glass wash: panel bg, goal strips, hovers */
  --holiday-accent: #0f766e;        /* lagoon teal */
}
@media (prefers-color-scheme: dark) {
  :root:where(:not([data-theme="light"])) body.holiday {
    --page: #0c110f;    /* lagoon night instead of pure black */
    --surface: #17201d;
    --grid: #2a332f;
    --baseline: #3c4741;
    --border: rgba(180, 240, 220, 0.10);
    --silhouette: #202a26;
    --band: rgba(45, 212, 191, 0.08);
    --holiday-accent: #2dd4bf;
    --holiday-fronds: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 240 190'%3E%3Cg transform='translate(236 4)'%3E%3Cg fill='%232dd4bf' fill-opacity='.08'%3E%3Cpath d='M0 0Q-90 -24 -176 -8Q-90 8 0 14Z' transform='rotate(-16)'/%3E%3Cpath d='M0 0Q-90 -24 -184 -4Q-90 10 0 14Z' transform='rotate(-42)'/%3E%3Cpath d='M0 0Q-84 -20 -160 0Q-84 12 0 14Z' transform='rotate(-68)'/%3E%3Cpath d='M0 0Q-76 -16 -132 8Q-72 16 0 14Z' transform='rotate(-88)'/%3E%3C/g%3E%3Cg fill='%23c9a97a' fill-opacity='.13'%3E%3Ccircle cx='-16' cy='18' r='9'/%3E%3Ccircle cx='-31' cy='11' r='7'/%3E%3C/g%3E%3C/g%3E%3C/svg%3E");
    --holiday-shore: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 26'%3E%3Cpath d='M0 12Q12 6 25 12T50 12T75 12T100 12V26H0Z' fill='%232dd4bf' fill-opacity='.05'/%3E%3Cpath d='M0 17Q13 23 25 17T50 17T75 17T100 17V26H0Z' fill='%232dd4bf' fill-opacity='.038'/%3E%3C/svg%3E");
    --holiday-vignette: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 150 90'%3E%3Ccircle cx='46' cy='22' r='15' fill='%23e8c35a' fill-opacity='.13'/%3E%3Cg transform='translate(148 2)' fill='%232dd4bf' fill-opacity='.07'%3E%3Cpath d='M0 0Q-62 -16 -118 -5Q-60 6 0 9Z' transform='rotate(-20)'/%3E%3Cpath d='M0 0Q-58 -13 -106 2Q-55 8 0 9Z' transform='rotate(-52)'/%3E%3Cpath d='M0 0Q-46 -10 -82 7Q-44 11 0 9Z' transform='rotate(-84)'/%3E%3C/g%3E%3C/svg%3E");
  }
}
:root[data-theme="dark"] body.holiday {
  --page: #0c110f;
  --surface: #17201d;
  --grid: #2a332f;
  --baseline: #3c4741;
  --border: rgba(180, 240, 220, 0.10);
  --silhouette: #202a26;
  --band: rgba(45, 212, 191, 0.08);
  --holiday-accent: #2dd4bf;
  --holiday-fronds: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 240 190'%3E%3Cg transform='translate(236 4)'%3E%3Cg fill='%232dd4bf' fill-opacity='.08'%3E%3Cpath d='M0 0Q-90 -24 -176 -8Q-90 8 0 14Z' transform='rotate(-16)'/%3E%3Cpath d='M0 0Q-90 -24 -184 -4Q-90 10 0 14Z' transform='rotate(-42)'/%3E%3Cpath d='M0 0Q-84 -20 -160 0Q-84 12 0 14Z' transform='rotate(-68)'/%3E%3Cpath d='M0 0Q-76 -16 -132 8Q-72 16 0 14Z' transform='rotate(-88)'/%3E%3C/g%3E%3Cg fill='%23c9a97a' fill-opacity='.13'%3E%3Ccircle cx='-16' cy='18' r='9'/%3E%3Ccircle cx='-31' cy='11' r='7'/%3E%3C/g%3E%3C/g%3E%3C/svg%3E");
  --holiday-shore: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 26'%3E%3Cpath d='M0 12Q12 6 25 12T50 12T75 12T100 12V26H0Z' fill='%232dd4bf' fill-opacity='.05'/%3E%3Cpath d='M0 17Q13 23 25 17T50 17T75 17T100 17V26H0Z' fill='%232dd4bf' fill-opacity='.038'/%3E%3C/svg%3E");
  --holiday-vignette: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 150 90'%3E%3Ccircle cx='46' cy='22' r='15' fill='%23e8c35a' fill-opacity='.13'/%3E%3Cg transform='translate(148 2)' fill='%232dd4bf' fill-opacity='.07'%3E%3Cpath d='M0 0Q-62 -16 -118 -5Q-60 6 0 9Z' transform='rotate(-20)'/%3E%3Cpath d='M0 0Q-58 -13 -106 2Q-55 8 0 9Z' transform='rotate(-52)'/%3E%3Cpath d='M0 0Q-46 -10 -82 7Q-44 11 0 9Z' transform='rotate(-84)'/%3E%3C/g%3E%3C/svg%3E");
}
/* Festive scenery (Rindo, 2026-08-14: "add vector graphics, like palm
   trees or coconuts … just some flair to show that it's holiday beyond
   just color" — option C of three mocked directions, his pick). Two
   pieces, both pure decoration:
   1. Page scenery: palm fronds (with a coconut pair at the crown) leaning
      in from the top-right of the viewport, and a low double wave along
      the bottom. It lives on a fixed z-index:-1 layer, so it can only
      ever paint where bare page background shows — the side gutters, the
      gaps between cards, the run-out below the last card. Every card is
      an opaque surface, which is what guarantees his one hard constraint
      ("I still want to be able to read all the text"): readability by
      construction, not by tuning opacity. On narrow desktop windows the
      gutters shrink and the scenery naturally recedes; the phone block
      hides it outright (no gutters at 375px — the panel vignette below is
      the phone's share of the flair).
   2. A faint sun-and-frond vignette in the plan panel's top-right corner,
      behind the 🏝 pill — the panel is where the mode lives, same
      reasoning as the teal frame below.
   Inline SVG data URIs: no asset files, no requests, gone with the class
   like every other holiday tint. Colours are baked per theme (a data URI
   can't read CSS variables), overridden in the two dark blocks below —
   same pattern as the tint variables. Nothing animates and nothing
   catches the pointer. Status colours untouched, as always. Opacities are
   this session's draft — tweak freely. */
body.holiday {
  --holiday-fronds: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 240 190'%3E%3Cg transform='translate(236 4)'%3E%3Cg fill='%230f766e' fill-opacity='.11'%3E%3Cpath d='M0 0Q-90 -24 -176 -8Q-90 8 0 14Z' transform='rotate(-16)'/%3E%3Cpath d='M0 0Q-90 -24 -184 -4Q-90 10 0 14Z' transform='rotate(-42)'/%3E%3Cpath d='M0 0Q-84 -20 -160 0Q-84 12 0 14Z' transform='rotate(-68)'/%3E%3Cpath d='M0 0Q-76 -16 -132 8Q-72 16 0 14Z' transform='rotate(-88)'/%3E%3C/g%3E%3Cg fill='%237a5b38' fill-opacity='.2'%3E%3Ccircle cx='-16' cy='18' r='9'/%3E%3Ccircle cx='-31' cy='11' r='7'/%3E%3C/g%3E%3C/g%3E%3C/svg%3E");
  --holiday-shore: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 26'%3E%3Cpath d='M0 12Q12 6 25 12T50 12T75 12T100 12V26H0Z' fill='%230f766e' fill-opacity='.08'/%3E%3Cpath d='M0 17Q13 23 25 17T50 17T75 17T100 17V26H0Z' fill='%230f766e' fill-opacity='.055'/%3E%3C/svg%3E");
  --holiday-vignette: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 150 90'%3E%3Ccircle cx='46' cy='22' r='15' fill='%23e8c35a' fill-opacity='.22'/%3E%3Cg transform='translate(148 2)' fill='%230f766e' fill-opacity='.09'%3E%3Cpath d='M0 0Q-62 -16 -118 -5Q-60 6 0 9Z' transform='rotate(-20)'/%3E%3Cpath d='M0 0Q-58 -13 -106 2Q-55 8 0 9Z' transform='rotate(-52)'/%3E%3Cpath d='M0 0Q-46 -10 -82 7Q-44 11 0 9Z' transform='rotate(-84)'/%3E%3C/g%3E%3C/svg%3E");
}
body.holiday::before {
  content: "";
  position: fixed; inset: 0; z-index: -1;
  pointer-events: none;
  background-image: var(--holiday-fronds), var(--holiday-shore);
  background-repeat: no-repeat, repeat-x;
  background-position: top -6px right -10px, left bottom;
  background-size: 240px 190px, auto 72px;
}
/* The plan panel is where the mode actually lives, so it carries the one
   clear accent: teal frame, teal title, teal 🏝 pill. Everything else only
   inherits the softened variables above. */
body.holiday .today-panel {
  border-color: var(--holiday-accent);
  background-image: var(--holiday-vignette);
  background-repeat: no-repeat;
  background-position: top right;
  background-size: 150px 90px;
}
body.holiday .today-title { color: var(--holiday-accent); }
body.holiday .today-mode .seg-btn.active {
  background: var(--holiday-accent); color: var(--surface);
}

* { box-sizing: border-box; }
[hidden] { display: none !important; }
html, body { margin: 0; padding: 0; }
body {
  background: var(--page);
  color: var(--text-primary);
  font-family: system-ui, -apple-system, "Segoe UI", sans-serif;
  font-size: 14px;
  line-height: 1.45;
}
.page { max-width: 1180px; margin: 0 auto; padding: 20px 24px 40px; }

/* ---------- header & filters ---------- */

/* Sticky on both widths since 2026-08-02 (Rindo: desktop should match the
   phone) — the phone bar has been sticky since 2026-07-31. Full-bleed over
   the page gutters via negative margins so cards slide under the bar rather
   than showing past its sides; the phone block overrides the metrics with
   its own tighter ones. */
.header {
  display: flex; justify-content: space-between; align-items: flex-start; gap: 16px;
  position: sticky; top: 0; z-index: 30;
  background: var(--page);
  margin: -20px -24px 14px; padding: 14px 24px 10px;
  border-bottom: 1px solid var(--border);
}
/* Wraps header + control row + tab bar so the PHONE can pin all three as one
   sticky block (Rindo, 2026-08-02 — mobile only, his words). On desktop the
   wrapper must not exist as a box: display-contents keeps the header a
   layout child of .page, so it sticks alone exactly as before. */
.top-chrome { display: contents; }
h1 {
  font-size: 20px; font-weight: 650; margin: 0;
  transition: opacity 0.25s; /* the rotating titles cross-fade (app.js) */
}
h1.title-fade { opacity: 0; }
/* The drill-down scrolls into view on row click — keep it clear of the
   sticky bar (the phone block narrows this to its own bar height). */
#drilldown { scroll-margin-top: 130px; }
/* Same idea for the stat-tile jump targets (Rindo, 2026-08-23): tapping
   Workout days or Body fat scrolls straight to its card, clear of the
   sticky header (the phone block below widens this to its own chrome
   height). */
#weeks-card, #bf-card { scroll-margin-top: 70px; }
.tagline { margin: 2px 0 0; color: var(--text-secondary); font-size: 13px; font-style: italic; }
.subtitle { margin: 2px 0 0; color: var(--text-muted); font-size: 12.5px; }

.filter-row {
  display: flex; flex-wrap: wrap; align-items: center; gap: 10px;
  margin-bottom: 14px;
}
.segmented { display: inline-flex; border: 1px solid var(--border); border-radius: 8px; overflow: hidden; background: var(--surface); }
.seg-btn {
  border: none; background: transparent; color: var(--text-secondary);
  padding: 6px 12px; font: inherit; font-size: 13px; cursor: pointer;
}
.seg-btn.active { background: var(--text-primary); color: var(--page); font-weight: 600; }
.seg-btn:not(.active):hover { background: var(--band); }
/* A session type the rotation hasn't come round to in a week or more. */
.seg-btn.seg-late:not(.active) { color: var(--status-under); }
.seg-btn.seg-late.active { box-shadow: inset 0 -3px 0 var(--status-under); }

.week-nav { display: inline-flex; align-items: center; gap: 6px; }
.window-label { min-width: 170px; text-align: center; font-weight: 600; font-variant-numeric: tabular-nums; }

.btn {
  border: 1px solid var(--border); background: var(--surface); color: var(--text-primary);
  border-radius: 8px; padding: 6px 12px; font: inherit; font-size: 13px; cursor: pointer;
}
.btn:hover { background: var(--band); }
.btn-icon { padding: 6px 10px; }
.btn-small { padding: 3px 10px; font-size: 12.5px; }

/* ---------- banners & stats ---------- */

.banner {
  display: flex; align-items: center; gap: 12px; flex-wrap: wrap;
  background: var(--surface); border: 1px solid var(--border); border-left: 3px solid var(--warn);
  border-radius: 8px; padding: 10px 14px; margin-bottom: 14px; color: var(--text-secondary);
}
.banner-error { border-left-color: var(--status-critical); color: var(--text-primary); }

.stats-row { display: flex; gap: 12px; flex-wrap: wrap; margin-bottom: 14px; }
.stat-tile {
  background: var(--surface); border: 1px solid var(--border); border-radius: 10px;
  padding: 10px 16px; display: flex; flex-direction: column; gap: 2px; min-width: 130px;
}
.stat-label { color: var(--text-muted); font-size: 12px; }
.stat-value { font-size: 26px; font-weight: 600; }
/* Tappable tiles (Rindo, 2026-08-23 rearrangement): Workout days jumps to
   the new weekly-workouts chart, Body fat jumps to the glide path. Muscles
   on track stays a plain tile — nowhere for it to jump to. */
.stat-link { cursor: pointer; }
.stat-link:hover, .stat-link:focus-visible { background: var(--band); outline: none; }
/* Renders under the label text — "Body fat" + a short "as of …" caption. */
.stat-caption { display: block; font-size: 10.5px; color: var(--text-muted); font-weight: 400; margin-top: 2px; }
/* Rindo, 2026-09-26: Last workout's value is a plain .stat-value (same size
   as its siblings) that only ever reads "N h" or "just now" — nowrap is a
   guard against the space in "just now" breaking at the narrowest phones. */
#stat-last-workout { white-space: nowrap; }
/* Its sub-line (session label, plus a verdict word for a scored gym day) is
   one line only, same spot as Muscles on track's "priority n/8" caption. */
#last-workout-sub { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-width: 100%; }
.public-desktop-tile, .public-body-row { display: none; }
.stat-reading { display: contents; }
.stat-delta { display: none; }

/* Scaffolding for the visitor Status view's titled sections (Rindo,
   2026-09-26 restructure: Cool Stats / Last 7 days / Trends). Both start
   inert for the owner at every width. .section-head carries the ordinary
   `hidden` attribute (index.html) — auth.js unhides the three of them for
   visitors at boot, the same way it unhides #btn-signin; a CSS class rule
   can't do that job on its own because Chromium's UA stylesheet applies
   `[hidden] { display: none !important }`. .trend-grid disappears via
   display:contents so #weeks-card/#bf-card stay direct children of
   #dashboard for the tab-visibility rules above and the phone order rules
   below — nothing here moves or duplicates a card, it only wraps two of
   them for the visitor's side-by-side row. */
.trend-grid { display: contents; }

/* Visitors never see the owner's controls or the mapping-debug card, at any
   width — desktop already hid the file/sync subtitles and the tab bar
   (2026-09-09); phones get the same treatment now so Refresh, the
   phone-view icon, the review-window row and a one-tab bar don't show up
   there either. */
body[data-access="visitor"] #btn-refresh,
body[data-access="visitor"] #btn-phone,
body[data-access="visitor"] .filter-row,
body[data-access="visitor"] #file-info,
body[data-access="visitor"] #sync-info,
body[data-access="visitor"] #tabs,
body[data-access="visitor"] #unmapped-card { display: none; }

/* Section headings: restrained, like a card's own h2, with a thin rule
   underneath. The caption beside "Cool Stats" is the scan-date text that
   used to live inside the stats row — same #body-row-caption node, same
   .h2-note styling the other card notes use, just moved up here; it wraps
   under the heading on narrow screens on its own, being inline text. Margin
   defaults to phone spacing (#dashboard is a plain flex column there); the
   desktop rule below zeroes it because the grid's own gap already spaces
   rows out, same "no double gap" rule the cards themselves follow. */
body[data-access="visitor"] .section-head {
  margin: 14px 0 8px; padding-bottom: 6px;
  border-bottom: 1px solid var(--border);
}
body[data-access="visitor"] .section-head h2 { margin: 0; font-size: 14px; font-weight: 650; }

@media (min-width: 641px) {
  body[data-access="visitor"] #dashboard[data-tab="status"] > #body-stats-row:not([hidden]),
  body[data-access="visitor"] #dashboard[data-tab="status"] > #activity-stats-row {
    display: grid; grid-template-columns: repeat(4, minmax(0, 1fr));
  }
  body[data-access="visitor"] .stats-row .stat-tile { min-width: 0; padding: 10px 12px; }
  body[data-access="visitor"] .public-desktop-tile { display: flex; }
  body[data-access="visitor"] #body-stats-row[hidden] { display: none; }
  body[data-access="visitor"] .stat-reading { display: flex; align-items: baseline; flex-wrap: wrap; gap: 6px; }
  body[data-access="visitor"] .stat-delta { display: inline; font-size: 12px; color: var(--text-muted); white-space: nowrap; }
  body[data-access="visitor"] .stat-delta.up { color: var(--status-on); }
  body[data-access="visitor"] .stat-delta.down { color: var(--status-critical); }
  body[data-access="visitor"] .stat-value { font-variant-numeric: tabular-nums; }
  body[data-access="visitor"] .stat-value:not(:only-child) { overflow-wrap: anywhere; }
  body[data-access="visitor"] #tile-bf .stat-caption { display: none; }

  /* One column of full-width sections. The two rows that need cards side by
     side (Body coverage/Sets per muscle, Workouts per week/Body fat %) get
     that from their own local grid (.main-grid, .trend-grid) instead of a
     named-area template here, so each row can use a different column split.
     Auto-placement also means a hidden ([hidden]/display:none) card — the
     usual state of #drilldown — costs no empty row or phantom gap. */
  body[data-access="visitor"] #dashboard[data-tab="status"] {
    display: grid;
    grid-template-columns: minmax(0, 1fr);
    gap: 14px;
  }
  /* Every direct child here is already spaced by the grid's own 14px gap;
     a card's own margin-bottom would stack on top of that and double it
     (Rindo, 2026-09-26). */
  body[data-access="visitor"] #dashboard[data-tab="status"] > #body-stats-row,
  body[data-access="visitor"] #dashboard[data-tab="status"] > #activity-stats-row,
  body[data-access="visitor"] #dashboard[data-tab="status"] > .main-grid,
  body[data-access="visitor"] #dashboard[data-tab="status"] > #drilldown,
  body[data-access="visitor"] #dashboard[data-tab="status"] > .trend-grid,
  body[data-access="visitor"] #dashboard[data-tab="status"] > #scorecard-card,
  body[data-access="visitor"] .section-head { margin: 0; }
  body[data-access="visitor"] .section-head { padding-bottom: 6px; }

  /* Body coverage + Sets per muscle: same height (align-items: stretch);
     Sets per muscle is compressed to fit at ≥901px below rather than left
     overflowing or clipped. Narrower than that, .main-grid's own ≤900px
     rule already stacks the pair into one column. */
  body[data-access="visitor"] #dashboard[data-tab="status"] > .main-grid { align-items: stretch; }
  body[data-access="visitor"] #dashboard[data-tab="status"] #heatmap-card,
  body[data-access="visitor"] #dashboard[data-tab="status"] #bars-card { margin-bottom: 0; }

  /* Workouts per week + Body fat %: two equal columns via their own
     mini-grid, so this row doesn't have to share a column split with the
     asymmetric one above. Single column until 901px (see below). */
  body[data-access="visitor"] #dashboard[data-tab="status"] > .trend-grid {
    display: grid; grid-template-columns: 1fr; gap: 14px;
  }
  body[data-access="visitor"] #dashboard[data-tab="status"] > .trend-grid > #weeks-card,
  body[data-access="visitor"] #dashboard[data-tab="status"] > .trend-grid > #bf-card { margin-bottom: 0; }
}
@media (min-width: 901px) {
  body[data-access="visitor"] #dashboard[data-tab="status"] > .trend-grid {
    grid-template-columns: 1fr 1fr;
  }
  /* The 12-week chart is half-width here: label alternate weeks so dates
     remain legible without removing any bars or changing phone/owner views. */
  body[data-access="visitor"] #weeks-card .wk-chart { gap: 4px; }
  body[data-access="visitor"] #weeks-card .wk-col:nth-child(even) .wk-label { visibility: hidden; }
  body[data-access="visitor"] #weeks-card .wk-col:nth-child(odd) .wk-label {
    width: calc(200% + 4px); text-align: left; overflow: visible; font-size: 9px;
  }

  /* Sets per muscle compressed to Body coverage's height (Rindo's ask):
     shrink row spacing and bar heights rather than clipping the list or
     letting it overflow the card. The explainer note is desktop reading
     that no longer fits the shorter row, same call already made for
     phones. */
  body[data-access="visitor"] #bars-card .bar-row,
  body[data-access="visitor"] #bars-card .bar-axis {
    grid-template-columns: 100px 1fr 62px 90px; gap: 6px; padding: 1px 4px;
  }
  body[data-access="visitor"] #bars-card .bar-row { min-height: 17px; }
  body[data-access="visitor"] #bars-card .bar-label-text { font-size: 10.5px; }
  /* Rindo, 2026-09-26: visitors must not see which muscles are priority —
     the badge is never appended for them, so there is nothing left to size. */
  body[data-access="visitor"] #bars-card .bar-track { height: 11px; }
  body[data-access="visitor"] #bars-card .bar-fill { top: 2px; height: 7px; }
  body[data-access="visitor"] #bars-card .bar-value,
  body[data-access="visitor"] #bars-card .bar-axis .bar-value { font-size: 10px; }
  body[data-access="visitor"] #bars-card .bar-value-target { font-size: 9.5px; }
  body[data-access="visitor"] #bars-card .bar-axis-track { height: 11px; }
  body[data-access="visitor"] #bars-card #bars { gap: 0; }
  body[data-access="visitor"] #bars-card .how-counting { display: none; }
}
@media (min-width: 641px) and (max-width: 800px) {
  body[data-access="visitor"] #body-stats-row .stat-value { font-size: 21px; }
  body[data-access="visitor"] #body-stats-row .stat-reading { gap: 3px; }
  body[data-access="visitor"] #body-stats-row .stat-delta { font-size: 10px; }
}

/* ---------- last-workout scorecard ---------- */

.score-summary { display: flex; flex-wrap: wrap; gap: 4px 22px; margin: 2px 0 8px; }
.score-stat { font-size: 12.5px; color: var(--text-muted); }
.score-stat b { font-size: 16px; font-weight: 650; color: var(--text-primary); font-variant-numeric: tabular-nums; }
.score-compare { margin: 0 0 6px; font-size: 12.5px; color: var(--text-secondary); }
.score-row {
  display: flex; align-items: baseline; gap: 10px;
  padding: 5px 0; border-bottom: 1px solid var(--grid);
}
.score-row:last-child { border-bottom: none; }
.score-name { flex: 1; font-weight: 600; font-size: 13.5px; min-width: 0; }
.score-sets { font-variant-numeric: tabular-nums; font-size: 12.5px; color: var(--text-secondary); white-space: nowrap; }
.score-top { font-variant-numeric: tabular-nums; font-size: 12.5px; color: var(--text-secondary); white-space: nowrap; min-width: 120px; text-align: right; }
.score-delta { font-variant-numeric: tabular-nums; font-size: 12px; white-space: nowrap; min-width: 110px; text-align: right; }
/* All-time best marker on an exercise row (Rindo, 2026-08-02). The gold is
   its own colour (--pb), off the status ramp — agent draft, tweak freely. */
.score-pb {
  font-size: 10px; font-weight: 700; text-transform: uppercase; letter-spacing: 0.05em;
  color: var(--pb); background: rgba(245, 200, 76, 0.12); border: 1px solid var(--pb);
  border-radius: 999px; padding: 1px 8px; white-space: nowrap;
}
.score-up { color: var(--status-on); font-weight: 600; }
.score-down { color: var(--status-under); font-weight: 600; }
.score-flat { color: var(--text-muted); }

/* Plan score (Rindo, 2026-07-31): the session vs that morning's draft,
   one row per muscle. The verdict became a METER on 2026-08-02, his ask:
   the grade section is a track filled to the plan score in the grade's
   colour, "like a progress meter", no visible numbers — the empty tail IS
   the message. Second pass, same day, also his: the "Verdict" cap is a
   darker shade of the grade's own colour (set from JS — "separate but, at
   the same time, integrated"), and the grade word rides the fill,
   right-justified at its leading edge. The word is drawn twice in identical
   geometry (two wrappers, both --fill wide): a muted copy on the track and
   a white copy clipped to the fill, so it reads at any fill width in both
   themes. `safe flex-end` drops the word back to the bar's start when the
   fill is too short to hold it. */
.plan-score { margin: 2px 0 10px; }
.plan-score-verdict {
  display: flex; width: 100%; margin: 2px 0 6px;
  border: 1px solid var(--border); border-radius: 999px; overflow: hidden;
  font-size: 12px; font-weight: 700; text-transform: uppercase; letter-spacing: 0.07em;
  white-space: nowrap;
}
.plan-score-verdict-label {
  flex: 0 0 auto; padding: 6px 16px;
  color: #fff; /* background: the grade colour at 0.55, from JS */
}
.plan-score-verdict-grade {
  flex: 1 1 auto; min-width: 0; position: relative; overflow: hidden;
  background: var(--band);
}
.plan-score-verdict-fill {
  position: absolute; left: 0; top: 0; bottom: 0; width: var(--fill);
}
/* In-flow, invisible: the only child in normal flow, so it is what gives
   the meter its height. Font metrics are inherited from the pill. */
.plan-score-verdict-ghost { visibility: hidden; display: block; padding: 6px 0; width: 0; }
.plan-score-verdict-word {
  position: absolute; left: 0; top: 0; bottom: 0; width: var(--fill);
  display: flex; align-items: center;
  justify-content: flex-end; justify-content: safe flex-end;
  padding: 0 12px; color: var(--text-secondary);
}
.plan-score-verdict-clip {
  position: absolute; left: 0; top: 0; bottom: 0; width: var(--fill);
  overflow: hidden;
}
.plan-score-verdict-clip .plan-score-verdict-word { width: 100%; }
.plan-score-verdict-word-fill { color: #fff; }
.plan-score-list { margin-top: 4px; }
/* The exercise list and the "Muscles covered" section each sit in their own
   box (Rindo, 2026-08-02): the sub-panel idiom the gym-plan panel uses —
   border, soft band tint, rounded corners. */
.score-list, .score-muscles {
  border: 1px solid var(--border); border-radius: 10px;
  background: var(--band); padding: 6px 12px; margin-top: 12px;
}
.score-muscles { padding-top: 9px; }
.score-muscles-title {
  margin: 0 0 3px; font-size: 11.5px; font-weight: 700; text-transform: uppercase;
  letter-spacing: 0.07em; color: var(--text-muted);
}
.plan-score-row {
  display: flex; align-items: baseline; gap: 10px;
  padding: 3px 0; border-bottom: 1px solid var(--grid); font-size: 12.5px;
}
.plan-score-row:last-child { border-bottom: none; }
.plan-score-name { flex: 1; font-weight: 550; min-width: 0; }
.plan-score-val { font-variant-numeric: tabular-nums; color: var(--text-secondary); white-space: nowrap; }
.plan-score-state { text-align: right; font-size: 12px; }
/* Same statement as the violet status colour — past the goal, dial back. */
.plan-score-past { color: var(--status-above); font-weight: 600; }
@media (max-width: 640px) {
  .score-row { flex-wrap: wrap; }
  .score-name { flex-basis: 100%; }
  .score-top, .score-delta { min-width: 0; }
}

/* ---------- cards & layout ---------- */

.main-grid { display: grid; grid-template-columns: minmax(300px, 380px) 1fr; gap: 14px; align-items: start; }
@media (max-width: 900px) { .main-grid { grid-template-columns: 1fr; } }

.card {
  background: var(--surface); border: 1px solid var(--border); border-radius: 12px;
  padding: 16px 18px; margin-bottom: 14px;
}
.card h2 { font-size: 14px; font-weight: 650; margin: 0 0 12px; }
.h2-note { font-weight: 400; color: var(--text-muted); font-size: 12px; margin-left: 6px; }

#dashboard.loading { opacity: 0.55; transition: opacity 0.15s; }

/* ---------- status chips ---------- */

.chip { display: inline-flex; align-items: center; gap: 5px; font-size: 12px; color: var(--text-secondary); white-space: nowrap; }
.chip-dot {
  display: inline-flex; align-items: center; justify-content: center;
  width: 15px; height: 15px; border-radius: 50%;
  font-size: 9px; line-height: 1; color: #fff; font-weight: 700;
}
.chip-critical .chip-dot { background: var(--status-critical); }
.chip-under .chip-dot { background: var(--status-under); }
.chip-on .chip-dot { background: var(--status-on); }
/* White ▲ on violet; the dark glyph was there for legibility on amber. */
.chip-above .chip-dot { background: var(--status-above); }
.chip-none .chip-dot { background: var(--status-none); }
.chip-ghost { visibility: hidden; }

/* ---------- bar chart ---------- */

#bars { display: flex; flex-direction: column; }
.bar-row, .bar-axis {
  display: grid; grid-template-columns: 128px 1fr 78px 108px;
  gap: 10px; align-items: center; padding: 3px 6px;
}
.bar-row { border-radius: 8px; cursor: pointer; min-height: 28px; }
.bar-row:hover, .bar-row:focus-visible { background: var(--band); outline: none; }
.bar-row.selected { background: var(--band); box-shadow: inset 0 0 0 1px var(--baseline); }

/* Column, not row (Rindo, 2026-09-26 priority tiers): a priority muscle
   stacks its "priority" tag above the name instead of squeezing both onto
   one line inside the fixed-width label column — the row just grows a
   little taller, same as any other wrap. */
.bar-label { display: flex; flex-direction: column; align-items: flex-end; text-align: right; }
.bar-label-text { font-size: 13px; color: var(--text-primary); }
.bar-label-priority { font-size: 10px; color: var(--text-muted); line-height: 1.3; }

.bar-track { position: relative; height: 22px; border-left: 1px solid var(--baseline); }
.bar-tick { position: absolute; top: 0; bottom: 0; width: 1px; background: var(--grid); }
.bar-band {
  position: absolute; top: 0; bottom: 0;
  background: var(--band);
  border-left: 1px solid var(--baseline); border-right: 1px solid var(--baseline);
}
.bar-fill {
  position: absolute; top: 4px; height: 14px; left: 0;
  border-radius: 0 4px 4px 0;
}
.status-critical { background: var(--status-critical); }
.status-under { background: var(--status-under); }
.status-on { background: var(--status-on); }
.status-none { background: var(--status-none); }
/* Matching glow on the bar, so the two views agree about which muscle is
   shouting. Softer than the body map's — a bar is already a big block of
   colour and doesn't need the same lift. */
.status-above {
  background: var(--status-above);
  box-shadow: 0 0 6px var(--status-above-glow);
}

.bar-value { font-variant-numeric: tabular-nums; white-space: nowrap; }
.bar-value-num { font-weight: 650; }
.bar-value-target { color: var(--text-muted); font-size: 12px; }

.bar-axis { padding-top: 2px; }
.bar-axis-track { position: relative; height: 16px; }
.bar-axis-tick {
  position: absolute; transform: translateX(-50%); top: 0;
  font-size: 11px; color: var(--text-muted); font-variant-numeric: tabular-nums;
}
.bar-axis .bar-value { color: var(--text-muted); font-size: 11px; font-weight: 400; padding-left: 8px; }

/* ---------- heatmap ---------- */

#heatmap { width: 100%; height: auto; display: block; }
.silhouette * { fill: var(--silhouette); }
.regions [data-group] {
  stroke: var(--surface); stroke-width: 1.5; cursor: pointer;
  transition: opacity 0.1s;
}
.regions [data-group]:hover { opacity: 0.8; }
.region-critical { fill: var(--status-critical); }
.region-under { fill: var(--status-under); }
.region-on { fill: var(--status-on); }
.region-none { fill: var(--status-none); }
/* No goal set *and* nothing logged. Was `var(--grid)`, which on the page
   background is close enough to nothing that Rindo read it as a rendering
   fault rather than a statement (2026-07-30). Now clearly a shape that is
   simply not being tracked. */
.region-empty { fill: var(--status-none); fill-opacity: 0.4; }

/* Past the goal glows (Rindo's ask, 2026-07-30) — the one status you are
   meant to notice from across the room and dial back. Breathes slowly rather
   than sitting still, because a static drop-shadow at this size just reads as
   a blurry edge. */
.region-above {
  fill: var(--status-above);
  animation: region-above-glow 2.6s ease-in-out infinite;
}
@keyframes region-above-glow {
  0%, 100% { filter: drop-shadow(0 0 2px var(--status-above-glow)); }
  50% { filter: drop-shadow(0 0 7px var(--status-above-glow)); }
}
@media (prefers-reduced-motion: reduce) {
  .region-above { animation: none; filter: drop-shadow(0 0 5px var(--status-above-glow)); }
}
.region-selected { stroke: var(--text-primary); stroke-width: 1.5; }
.figure-caption { fill: var(--text-muted); font-size: 12px; }

.legend { display: flex; flex-wrap: wrap; gap: 10px 14px; margin-top: 12px; }

/* ---------- gap plan ---------- */

.gap-section { margin-top: 14px; }
.gap-section:first-child { margin-top: 0; }
.gap-section {
  border: 1px solid var(--border); border-radius: 10px;
  padding: 10px 14px 8px; background: var(--band);
}
.gap-section + .gap-section { margin-top: 10px; }
.gap-section-title {
  display: flex; align-items: center; gap: 10px; margin: 0 0 2px;
  font-size: 11.5px; color: var(--text-muted);
}
.gap-section-badge {
  display: inline-block; padding: 3px 12px; border-radius: 999px;
  background: var(--text-primary); color: var(--page);
  font-size: 11px; font-weight: 700; text-transform: uppercase; letter-spacing: 0.07em;
}
.gap-section-count { font-weight: 400; }
.gap-row { padding: 9px 0; border-bottom: 1px solid var(--grid); }
.gap-row:last-child { border-bottom: none; padding-bottom: 2px; }
.gap-head { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
.gap-group {
  border: none; background: none; padding: 0; font: inherit; font-size: 13.5px;
  font-weight: 650; color: var(--text-primary); cursor: pointer;
}
.gap-group:hover { text-decoration: underline; }
.gap-need { color: var(--text-secondary); font-size: 12.5px; font-variant-numeric: tabular-nums; }
.gap-options { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 6px; }
.gap-pill {
  display: inline-flex; align-items: baseline; gap: 6px;
  border: 1px solid var(--border); border-radius: 999px;
  padding: 3px 10px; font-size: 12.5px; background: var(--band);
}
.gap-pill-new { background: transparent; border-style: dashed; }
.gap-pill-name { font-weight: 550; }
.gap-pill-sets { font-variant-numeric: tabular-nums; color: var(--text-secondary); }
.gap-pill-note { color: var(--text-muted); font-size: 11.5px; }
.gaps-clear { color: var(--status-on); font-weight: 600; margin: 0; }
#gaps-body p.muted { margin: 0; }
.gap-legend { color: var(--text-muted); font-size: 11.5px; margin: 8px 0 0; }

/* ---------- body fat glide path (Rindo, 2026-08-11) ---------- */
/* One series, one axis: the line wears --series-1, the glide is a dashed
   reference in muted ink, all text wears text tokens (dataviz rules). */
.bf-head { display: flex; align-items: baseline; gap: 10px; flex-wrap: wrap; margin-bottom: 4px; }
.bf-now { font-size: 26px; font-weight: 700; font-variant-numeric: tabular-nums; color: var(--text-primary); }
.bf-now-date { color: var(--text-muted); font-size: 12px; }
#bf-chart { display: block; width: 100%; height: auto; }
.bf-grid { stroke: var(--grid); stroke-width: 1; }
.bf-tick { fill: var(--text-muted); font-size: 10.5px; }
.bf-line { fill: none; stroke: var(--series-1); stroke-width: 2; stroke-linejoin: round; stroke-linecap: round; }
.bf-dot { fill: var(--series-1); stroke: var(--surface); stroke-width: 2; }
.bf-glide { fill: none; stroke: var(--text-muted); stroke-width: 1.5; stroke-dasharray: 4 4; }
.bf-goal-dot { fill: var(--surface); stroke: var(--text-secondary); stroke-width: 1.5; }
.bf-label { fill: var(--text-secondary); font-size: 11px; font-weight: 600; }
.bf-label-muted { fill: var(--text-muted); font-size: 10.5px; }

/* ---------- workouts per week (Rindo, 2026-08-23 rearrangement) ----------
   app.js emits one .wk-col per week, each a stack of .wk-block divs (one
   per logged session that week) glued to a .wk-label. Block colours arrive
   inline from JS (the score colours) — everything else here is theme
   tokens, and the block sizing lives in these CSS custom properties so the
   goal-line math below can share them. */
.wk-chart {
  position: relative; display: flex; align-items: flex-end; gap: 6px;
  --wk-block: 16px; --wk-gap: 2px; --wk-label-h: 18px;
  padding-top: 10px;
}
.wk-col { flex: 1 1 0; min-width: 0; display: flex; flex-direction: column; justify-content: flex-end; }
.wk-bar {
  display: flex; flex-direction: column-reverse; gap: var(--wk-gap);
  /* Reserves ~6 rows of headroom so the dashed goal line always has
     somewhere to sit, even on a week with zero logged sessions. */
  min-height: calc(var(--wk-block) * 6 + var(--wk-gap) * 5);
  /* column-reverse flips which edge is main-start: it's the BOTTOM here,
     not the top. Blocks are appended chronologically (oldest first) and
     this default-equivalent flex-start packs them tight against that
     bottom edge — right above the label — leaving the reserved headroom
     empty at the top, which is what makes the bar look like it "grows
     upward from the label" instead of floating mid-column. */
  justify-content: flex-start;
}
.wk-block { height: var(--wk-block); border-radius: 3px; }
/* Personal-best marker (Rindo, 2026-09-27): a gold dog-eared corner on any
   workout-day block with >=1 all-time best that session, plus an "anime
   glasses glint" — a light streak sweeping the block, timed with a tiny
   sparkle on the notch. The notch is sized from the block's height, so it
   fits the 12px visitor blocks and the 16px owner blocks without shifting
   layout. */
/* The block itself does not clip, so the sparkle can pop past the corner;
   the glint span is the clipping box and the streak is its ::before. */
/* The block itself does not clip, so the sparkle can pop past the corner;
   the glint span is the clipping box and the streak is its ::before. */
.wk-block.is-pb { position: relative; }
.wk-block.is-pb::after {
  content: ''; position: absolute; top: 0; right: 0;
  height: 62%; aspect-ratio: 1; max-height: 12px;
  border-top-right-radius: 3px;
  background: var(--pb);
  clip-path: polygon(0 0, 100% 0, 100% 100%);
  pointer-events: none;
}
.wk-block.is-pb::before {
  /* Anime sparkle, drawn like the sparkles emoji: one big four-point star
     with curved sides and a white core, plus two small ones. */
  content: ''; position: absolute; top: -9px; right: -9px; z-index: 2;
  width: 16px; height: 16px;
  background: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath fill='%23ffd84a' d='M10 1 Q11 9 19 10 Q11 11 10 19 Q9 11 1 10 Q9 9 10 1Z'/%3E%3Cpath fill='%23fff' d='M10 6 Q10.5 9.5 14 10 Q10.5 10.5 10 14 Q9.5 10.5 6 10 Q9.5 9.5 10 6Z'/%3E%3Cpath fill='%23ffd84a' d='M19.5 13 Q20 16 23 16.5 Q20 17 19.5 20 Q19 17 16 16.5 Q19 16 19.5 13Z'/%3E%3Cpath fill='%23ffd84a' d='M19 1 Q19.3 3.7 22 4 Q19.3 4.3 19 7 Q18.7 4.3 16 4 Q18.7 3.7 19 1Z'/%3E%3C/svg%3E") center / contain no-repeat;
  transform: scale(0); opacity: 0;
  animation: wk-pb-sparkle 6s ease-out infinite;
  animation-delay: calc(var(--pb-i, 0) * 0.3s);
  pointer-events: none;
}
.wk-pb-glint {
  position: absolute; inset: 0; overflow: hidden; border-radius: inherit;
  pointer-events: none;
}
.wk-pb-glint::before {
  /* The anime "glasses catch the light" swipe: a bright bar and a thin
     twin, travelling from the bottom-left corner to the top-right. */
  content: ''; position: absolute; top: -100%; left: -100%; width: 300%; height: 300%;
  background: linear-gradient(45deg,
    transparent 0 44%, rgba(255, 255, 255, 0.95) 44% 48%,
    transparent 48% 50%, rgba(255, 255, 255, 0.8) 50% 51.5%, transparent 51.5%);
  transform: translate(-45%, 45%);
  animation: wk-pb-sweep 6s ease-in-out infinite;
  animation-delay: calc(var(--pb-i, 0) * 0.3s);
}
@keyframes wk-pb-sweep {
  0% { transform: translate(-45%, 45%); }
  7%, 100% { transform: translate(45%, -45%); }
}
@keyframes wk-pb-sparkle {
  0%, 6% { transform: scale(0) rotate(-30deg); opacity: 0; }
  8.5% { transform: scale(1.15) rotate(0deg); opacity: 1; }
  12%, 100% { transform: scale(0) rotate(20deg); opacity: 0; }
}
@media (prefers-reduced-motion: reduce) {
  .wk-block.is-pb::before, .wk-pb-glint::before { animation: none; opacity: 0; }
}
.wk-legend {
  display: flex; align-items: center; gap: 6px;
  color: var(--text-secondary); font-size: 12px;
}
.wk-legend-swatch {
  position: relative; width: 14px; height: 14px; border-radius: 3px;
  background: var(--baseline); flex: 0 0 auto;
}
.wk-legend-swatch::after {
  content: ''; position: absolute; top: 0; right: 0; width: 62%; height: 62%;
  border-top-right-radius: 3px;
  background: var(--pb);
  clip-path: polygon(0 0, 100% 0, 100% 100%);
}
.wk-label {
  height: var(--wk-label-h); font-size: 10.5px; color: var(--text-muted);
  text-align: center; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
  padding-top: 4px;
}
/* JS sets --wk-goal-n (the week's workout-count goal) on .wk-chart; the
   line sits at the top edge of that many stacked blocks, split across the
   gap above it so it reads as resting ON block N rather than through it. */
.wk-goal {
  position: absolute; left: 0; right: 0; border-top: 1.5px dashed var(--baseline);
  bottom: calc(var(--wk-label-h) + var(--wk-goal-n, 4) * (var(--wk-block) + var(--wk-gap)) - var(--wk-gap) / 2);
}
/* Grade breakdown, moved below the chart (Rindo, 2026-09-26): one line per
   grade instead of the old inline " · "-joined string next to the title. */
.wk-breakdown { margin-top: 10px; display: flex; flex-direction: column; gap: 2px; font-size: 12px; }

.how-counting { margin-top: 10px; color: var(--text-secondary); font-size: 12.5px; }
.how-counting summary { cursor: pointer; color: var(--text-muted); font-size: 12px; }
.how-counting summary:hover { color: var(--text-secondary); }
.how-counting p { margin: 6px 0 0; }

/* ---------- this week's plan ---------- */

.today-panel {
  border: 1px solid var(--border); border-radius: 10px;
  padding: 12px 14px; margin-bottom: 12px; background: var(--band);
}
.today-head { display: flex; align-items: baseline; gap: 10px; flex-wrap: wrap; }
.today-title {
  margin: 0; font-size: 11.5px; font-weight: 700; text-transform: uppercase;
  letter-spacing: 0.07em; color: var(--text-muted);
}
/* Sign-in gate — deployed build only. Covers the page rather than sitting
   inside it, so nothing renders before there is a session that can read. */
.auth-gate {
  position: fixed; inset: 0; z-index: 100;
  display: flex; align-items: center; justify-content: center;
  background: var(--page); padding: 24px;
}
.auth-box {
  max-width: 380px; width: 100%; text-align: center;
  background: var(--surface); border: 1px solid var(--border);
  border-radius: 14px; padding: 32px 24px;
}
.auth-box h2 { margin: 0 0 8px; font-size: 19px; }
.auth-detail { margin: 0 0 18px; font-size: 13px; color: var(--text-muted); }
.auth-btn { width: 100%; justify-content: center; padding: 10px 16px; }

/* Landscape guard (Rindo's Galaxy S24+, Chrome, 2026-09-28): the dashboard
   is built tall, not wide, and a plain browser tab has no orientation lock
   to fall back on (that's manifest.json + orientation.js, for the app once
   installed to the home screen). z-index 300: above about.css's curtain
   and close button (200/210), the highest anywhere else in the app.
   Guarded by hover:none + pointer:coarse so it never fires on a desktop
   window narrowed to a wide-short shape, and the phone-preview iframe is a
   fixed 384x832 portrait frame, so the orientation query is false inside
   it regardless. */
#rotate-guard { display: none; }
@media (orientation: landscape) and (max-height: 500px) and (hover: none) and (pointer: coarse) {
  body { overflow: hidden; }
  #rotate-guard {
    display: flex; flex-direction: column; align-items: center; justify-content: center;
    position: fixed; inset: 0; z-index: 300;
    background: var(--page); color: var(--text-primary);
    padding: 24px; text-align: center; gap: 10px;
  }
}
.rotate-guard-glyph { font-size: 40px; line-height: 1; transform: rotate(90deg); }
.rotate-guard-line { margin: 0; font-size: 16px; font-weight: 600; }
.rotate-guard-sub { margin: 0; font-size: 13px; color: var(--text-muted); }

.header-actions { display: flex; gap: 8px; align-items: flex-start; flex-wrap: wrap; }
.btn-quiet { color: var(--text-muted); }

/* Desktop has no hamburger: the toggle hides and the "menu" dissolves into
   the flex row above (display: contents), so the buttons sit inline exactly
   where they always have. The phone block turns it into a dropdown. */
.menu-toggle { display: none; }
.header-menu { display: contents; }

/* The phone-view button is glyph-only (Rindo, 2026-08-02: "don't call it
   Phone View, and don't put that icon. Just put in a monochrome phone
   icon"). The svg strokes currentColor, and 16px + 7px padding matches the
   neighbouring buttons' height. */
#btn-phone { padding: 7px 9px; }
#btn-phone svg { display: block; width: 16px; height: 16px; }

/* Phone view (Rindo, 2026-08-02): desktop-only overlay with the page in an
   S24+-sized iframe, for showing people the mobile layout. The frame keeps
   its real 384×832 viewport and is *scaled* to fit short windows
   (--phone-scale, set from app.js) — transform doesn't touch the iframe's
   media queries, so the phone CSS stays honest. z-index 90: over everything
   except the sign-in gate (100), which must win when signed out. */
.phone-preview {
  position: fixed; inset: 0; z-index: 90;
  /* Near-opaque on purpose (Rindo, 2026-08-02: the desktop page showing
     through a 0.62 backdrop beside the frame read as the phone page
     "extended past the visible screen"). The phone stands alone now. */
  background: rgba(10, 13, 18, 0.97);
}
/* The page inside the frame (app.js adds the class when framed) drops its
   classic scrollbar, like DevTools device mode: a Windows scrollbar ate
   ~15px of the 384 on tabs tall enough to scroll, so panel widths shifted
   between tabs (Rindo, 2026-08-02). Real phones use zero-width overlay
   scrollbars — no visible scrollbar IS the S24+ render. Wheel, drag and
   keyboard scrolling are unaffected. */
html.in-phone-frame { scrollbar-width: none; }
html.in-phone-frame::-webkit-scrollbar { display: none; }
.phone-frame {
  position: fixed; top: 50%; left: 50%;
  transform: translate(-50%, -50%) scale(var(--phone-scale, 1));
  /* content-box, against the global border-box: 384×832 must be the window
     the iframe shows through, with the bezel border OUTSIDE it. Under
     border-box the borders ate into it and the bezel hid the page's right
     20px — right-aligned content clipped on every tab, worst on the
     scorecard (Rindo's screenshots, 2026-08-02). Outer box is 404×852,
     which is what the fit math in app.js already assumed. */
  box-sizing: content-box;
  width: 384px; height: 832px;
  background: var(--page);
  border: 10px solid #14181f; border-radius: 30px;
  box-shadow: 0 18px 60px rgba(0, 0, 0, 0.45);
  overflow: hidden;
}
.phone-frame iframe { display: block; width: 384px; height: 832px; border: none; }
.phone-caption {
  position: fixed; bottom: 10px; left: 0; right: 0; margin: 0;
  text-align: center; font-size: 12px; color: var(--text-secondary);
}
.phone-close { position: fixed; top: 14px; right: 18px; font-size: 14px; }

/* Supabase mirror status line (header). Silent when all is well is not an
   option here — a failed push is the one thing that must be visible. */
.sync-line { font-size: 11.5px; color: var(--text-muted); margin-top: 2px; }
.sync-line.sync-error { color: var(--status-critical); font-weight: 600; }

.today-why { font-size: 12px; color: var(--text-muted); }
.today-overdue { color: var(--status-under); }
/* The holiday switch, right-aligned on the title line (Rindo, 2026-08-01;
   the reasoning is the 2026-07-29 one that put the old time pills here: a
   control row costs a phone line, the title line has the room). The time
   pills themselves left with the 60/90 choice on 2026-08-06. The phone
   block re-`order`s the switch up next to the title, where the "why" lines
   are forced to 100% width. */
.today-mode { margin-left: auto; align-self: center; }
.today-controls { display: flex; gap: 10px; flex-wrap: wrap; margin: 8px 0 4px; }
.today-list { margin-top: 4px; }
.today-row {
  display: flex; align-items: baseline; gap: 10px;
  padding: 5px 0; border-bottom: 1px solid var(--grid);
}
.today-row:last-child { border-bottom: none; }
.today-ex { flex: 1; display: flex; align-items: baseline; gap: 7px; flex-wrap: wrap; min-width: 0; }
.today-num { color: var(--text-muted); font-variant-numeric: tabular-nums; font-size: 12px; }
.today-name { font-weight: 600; font-size: 13.5px; }
.today-note { color: var(--text-muted); font-size: 11.5px; }
.today-scheme { font-variant-numeric: tabular-nums; font-size: 12.5px; color: var(--text-secondary); white-space: nowrap; }
.today-weight { font-variant-numeric: tabular-nums; font-size: 12px; color: var(--text-muted); white-space: nowrap; min-width: 105px; text-align: right; }
.today-skip .today-name, .today-cut .today-name { color: var(--text-muted); font-weight: 500; text-decoration: line-through; }
.today-cut .today-name { text-decoration: none; }
.today-extra .today-name::after { content: " +"; color: var(--status-on); font-weight: 700; }
/* The core box (Rindo, 2026-08-06): core on every gym day, a separate boxed
   section under the day's list — "I complete the workout and then I do the
   core next". Sub-panel idiom (border, band tint, rounded corners), same as
   the scorecard's boxes. */
.today-core {
  border: 1px solid var(--border); border-radius: 10px;
  background: var(--band); padding: 6px 12px 8px; margin-top: 10px;
}
.today-core-title {
  margin: 2px 0 0; font-size: 11.5px; font-weight: 700; text-transform: uppercase;
  letter-spacing: 0.07em; color: var(--text-muted);
}
.today-core .today-list { margin-top: 0; }
.today-foot { margin: 8px 0 0; font-size: 12px; }
/* The rotation strip and the per-exercise progression line (Rindo's spec,
   2026-09-08 §25). The strip is chips joined by arrows; NEXT is filled. */
.today-rotation { display: flex; align-items: center; gap: 5px; flex-wrap: wrap; width: 100%; margin: 2px 0 0; font-size: 12px; }
.today-rot-step { border: 1px solid var(--border); border-radius: 999px; padding: 1px 9px; background: var(--band); color: var(--text-secondary); font-variant-numeric: tabular-nums; }
.today-rot-step.next { background: var(--status-on); color: #fff; border-color: transparent; font-weight: 650; }
.today-rot-arrow { color: var(--text-muted); }
.today-prog { flex-basis: 100%; color: var(--text-secondary); font-size: 11.5px; font-variant-numeric: tabular-nums; }
.today-prog-increase_weight { color: var(--status-on); }
.today-prog-regression { color: var(--status-under); }
.today-prog-insufficient_data { color: var(--text-muted); }
.today-paired { border-top: 1px dashed var(--border); margin-top: -1px; }
.today-paired .today-num::before { content: "⇄ "; color: var(--text-muted); }
.plan-week-title {
  margin: 6px 0 2px; font-size: 11.5px; font-weight: 700; text-transform: uppercase;
  letter-spacing: 0.07em; color: var(--text-muted);
}
@media (max-width: 640px) {
  .today-weight { min-width: 0; }
}

.plan-row {
  display: flex; align-items: baseline; gap: 12px;
  padding: 8px 0; border-bottom: 1px solid var(--grid);
}
.plan-row:last-child { border-bottom: none; padding-bottom: 2px; }
.plan-row-label {
  flex: 0 0 64px; font-size: 11.5px; font-weight: 700; text-transform: uppercase;
  letter-spacing: 0.07em; color: var(--text-muted);
}
.plan-items { display: flex; flex-wrap: wrap; gap: 6px; }
.plan-item {
  display: inline-flex; align-items: baseline; gap: 6px;
  border: 1px solid var(--border); border-radius: 999px;
  padding: 3px 10px; font-size: 12.5px; background: var(--band);
}
.plan-item-name { font-weight: 550; }
.plan-item-note { color: var(--text-muted); font-size: 11.5px; font-variant-numeric: tabular-nums; }
.plan-done .plan-item-note { color: var(--status-on); font-weight: 600; }
.plan-todo { background: transparent; border-style: dashed; }
.plan-optional { background: transparent; color: var(--text-muted); border-style: dashed; }
.plan-subnote { margin: 4px 0 0 76px; font-size: 11.5px; }
.plan-muscles { font-size: 12.5px; color: var(--text-secondary); }
@media (max-width: 640px) {
  .plan-row { flex-direction: column; gap: 4px; }
  .plan-subnote { margin-left: 0; }
}

/* ---------- running ---------- */

.run-line { margin: 0 0 4px; color: var(--text-secondary); }
.run-line .run-value { font-weight: 650; color: var(--text-primary); font-variant-numeric: tabular-nums; }
.run-note { margin: 8px 0 0; font-size: 12.5px; }
.run-note a { color: var(--text-primary); }

/* ---------- drill-down ---------- */

#drilldown-body { overflow-x: auto; }

.drilldown-head { display: flex; justify-content: space-between; align-items: center; gap: 10px; }
.drilldown-head h2 { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; margin: 0; }
.drilldown-group { font-size: 15px; }
.drilldown-sub { font-weight: 400; color: var(--text-secondary); }
#drilldown-body { margin-top: 10px; }

.drill-table { width: 100%; border-collapse: collapse; font-size: 13px; }
.drill-table th {
  text-align: left; font-weight: 600; color: var(--text-muted); font-size: 12px;
  padding: 6px 10px; border-bottom: 1px solid var(--grid);
}
.drill-table td { padding: 6px 10px; border-bottom: 1px solid var(--grid); vertical-align: top; }
.drill-table tr:last-child td { border-bottom: none; }
.drill-table .num { text-align: right; font-variant-numeric: tabular-nums; }
.drill-table .strong { font-weight: 650; }
.drill-exercise { font-weight: 500; }
.drill-days { color: var(--text-secondary); font-size: 12.5px; }
.muted { color: var(--text-muted); }

/* ---------- unmapped ---------- */

.unmapped-section-title { color: var(--text-muted); font-size: 12px; margin: 10px 0 4px; }
.unmapped-section-title:first-child { margin-top: 0; }
.unmapped-item { display: flex; justify-content: space-between; gap: 12px; padding: 3px 0; }
.unmapped-count { color: var(--text-muted); font-variant-numeric: tabular-nums; }

/* ============================================================
   Phone layout (Rindo, 2026-07-28)
   ------------------------------------------------------------
   The deployed build is read at the gym, one-handed. Measured before
   this block: 6,118px on a 375px screen — 7.5 screens of scrolling,
   and the tallest card was "To be done this week", which he never asked
   to see. Desktop is deliberately untouched; everything here is inside
   the 640px query.

   Order he asked for: body coverage · sets per muscle · this week's
   plan · last workout. Everything else demoted or collapsed.
   ============================================================ */

/* Short button labels: swapped in below, hidden by default. */
.lbl-short { display: none; }

/* Collapsible cards (desktop keeps them open; the caret only shows where
   the card can actually collapse). */
.card-caret { display: none; }
.card-collapsible.collapsed > :not(h2) { display: none; }

/* ---------- tabs: both widths ----------
   Phone-only from 2026-07-28 to 2026-08-02, when Rindo asked for the desktop
   to match the phone ("just about being a little more consistent, having a
   cohesive design") — his explicit yes that day reverses the 2026-07-28
   "desktop stays one page" call (AGENTS.md decision log). Rindo's 2026-08-23
   rearrangement dropped Last workout back to three tabs and folded its
   scorecard into Status as a one-stop shop, appended at the bottom below the
   drill-down, the new weekly-workouts chart and the BF% card. Status now
   carries everything except the Plan panel, the gaps list and the live
   tracker; Plan is what he reads standing in the gym; Track is the live
   logger. Cards are only shown or hidden — no card is rendered twice and
   app.js has no per-tab rendering branch. */
.tabs {
  display: flex; gap: 3px; margin-bottom: 10px; padding: 3px;
  background: var(--surface); border: 1px solid var(--border); border-radius: 10px;
}
.tab {
  flex: 1 1 0; min-width: 0;
  border: none; background: transparent; color: var(--text-secondary);
  border-radius: 7px; padding: 7px 4px; font: inherit; font-size: 12.5px;
  cursor: pointer; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.tab.active { background: var(--text-primary); color: var(--page); font-weight: 650; }

/* Slide-in cue on tab switch (Rindo, 2026-08-01: "it needs to feel like
   it's a swipe"; on desktop the same cue answers a click). A motion cue,
   not a carousel — cards are still only shown or hidden. Reduced-motion
   users get the instant switch (app.js never adds these classes). */
@keyframes tab-in-from-right {
  from { opacity: 0.35; transform: translateX(30px); }
  to { opacity: 1; transform: none; }
}
@keyframes tab-in-from-left {
  from { opacity: 0.35; transform: translateX(-30px); }
  to { opacity: 1; transform: none; }
}
#dashboard.tab-in-right { animation: tab-in-from-right 0.22s ease-out; }
#dashboard.tab-in-left { animation: tab-in-from-left 0.22s ease-out; }

/* Status: everything shows except the Plan panel, the gaps list and the
   live tracker — the scorecard and the drill-down are Status residents now
   (2026-08-23), and "Not counted" stays Status-only diagnostics (desktop
   only; the phone block hides it outright regardless of tab). */
#dashboard[data-tab="status"] > #plan-card,
#dashboard[data-tab="status"] > #gaps-card,
#dashboard[data-tab="status"] > #tracker-card { display: none; }

/* Plan: only the plan panel and the gaps list show. Visitors never reach
   this tab (auth.js hides it), so the section headings and the weeks/bf
   wrapper only ever need hiding here for completeness. */
#dashboard[data-tab="plan"] > .stats-row,
#dashboard[data-tab="plan"] > #body-stats-row,
#dashboard[data-tab="plan"] > .section-head,
#dashboard[data-tab="plan"] > .main-grid,
#dashboard[data-tab="plan"] > #drilldown,
#dashboard[data-tab="plan"] > .trend-grid,
#dashboard[data-tab="plan"] > #scorecard-card,
#dashboard[data-tab="plan"] > #unmapped-card,
#dashboard[data-tab="plan"] > #tracker-card { display: none; }

/* Track, the third tab (Rindo, 2026-08-23): the live tracker card and
   nothing else — same show/hide-only mechanism, nothing renders twice. */
#dashboard[data-tab="track"] > .stats-row,
#dashboard[data-tab="track"] > #body-stats-row,
#dashboard[data-tab="track"] > .section-head,
#dashboard[data-tab="track"] > .main-grid,
#dashboard[data-tab="track"] > #drilldown,
#dashboard[data-tab="track"] > .trend-grid,
#dashboard[data-tab="track"] > #plan-card,
#dashboard[data-tab="track"] > #gaps-card,
#dashboard[data-tab="track"] > #scorecard-card,
#dashboard[data-tab="track"] > #unmapped-card { display: none; }

/* ---------- live tracker (Rindo, 2026-08-23; proposal-live-tracker.md) ----------
   Strong-style set logging. All chrome runs on the theme tokens, so light,
   dark and holiday mode need nothing extra here. */

/* The + on every Today-panel exercise row: pushes that one exercise into
   the live session (his correction: per exercise, not per day). */
.today-add {
  flex: none; align-self: center;
  width: 24px; height: 24px; border-radius: 50%;
  border: 1px solid var(--border); background: var(--surface);
  color: var(--text-secondary); font: inherit; font-size: 15px; line-height: 1;
  cursor: pointer; padding: 0;
}
.today-add:hover { color: var(--text-primary); border-color: var(--text-muted); }
.today-add.in { color: var(--status-on); border-color: var(--status-on); }

/* The whole-day add (Rindo, 2026-08-25): one tap freezes the drafted day
   into the live session; reads ✓ while a session holds the day. */
.today-addall-row { margin: 8px 0 2px; }
.today-addall { font-size: 12.5px; }
.today-addall.in { color: var(--status-on); border-color: var(--status-on); }

/* Rest timer bar. The cue at zero is visual only (F5: no sound in the
   browser) — a short green pulse plus the border. */
.tr-timer {
  display: flex; align-items: center; gap: 8px; flex-wrap: wrap;
  padding: 8px 10px; margin-bottom: 12px;
  background: var(--surface); border: 1px solid var(--border); border-radius: 9px;
}
.tr-timer-txt { font-variant-numeric: tabular-nums; font-size: 13px; font-weight: 600; }
.tr-timer.run { border-color: var(--text-secondary); }
.tr-timer.flash { border-color: var(--status-on); animation: tr-flash 0.9s ease-in-out 4; }
@keyframes tr-flash {
  0%, 100% { background: var(--surface); }
  50% { background: rgba(12, 163, 12, 0.18); }
}
@media (prefers-reduced-motion: reduce) {
  .tr-timer.flash { animation: none; }
}
.tr-sync { margin-left: auto; font-size: 11px; }

/* Rindo, 2026-09-15: live verdict meter, top of the Track tab, right under
   the rest timer — same .plan-score-verdict* markup and classes as the
   Status-tab scorecard's meter (buildVerdictMeter in app.js), just a second
   mount point. Full width so it fits ~400px with no horizontal scroll: the
   meter itself already shrinks its fill section via min-width: 0. */
.tr-verdict { margin: 0 0 12px; }
.tr-verdict .score-compare { margin: 0; }

.tr-entry {
  border: 1px solid var(--grid); border-radius: 9px;
  padding: 8px 10px; margin-bottom: 10px;
}
.tr-entry-head { display: flex; align-items: flex-start; justify-content: space-between; gap: 6px 8px; flex-wrap: wrap; }
.tr-entry-title { display: flex; align-items: baseline; gap: 7px; flex-wrap: wrap; min-width: 0; }
.tr-entry-name { font-weight: 600; font-size: 13.5px; }
.tr-entry-target { color: var(--text-secondary); font-size: 12px; font-variant-numeric: tabular-nums; white-space: nowrap; }
.tr-entry-last { color: var(--text-muted); font-size: 11.5px; }
.tr-entry-prog { flex-basis: 100%; color: var(--text-secondary); font-size: 11.5px; font-variant-numeric: tabular-nums; }
.tr-prog-increase_weight { color: var(--status-on); }
.tr-prog-regression { color: var(--status-under); }
.tr-entry-rest { font-size: 11px; white-space: nowrap; }
/* .tr-in-rir removed 2026-09-25 with the RIR box (Rindo: weight + reps
   (+ seconds) only). */
/* Add-weight nudge badge (Rindo, 2026-09-25): louder than .tr-entry-prog,
   same status-on green the progression line already uses for this verdict. */
.tr-nudge {
  flex-basis: 100%; display: inline-block; margin-top: 1px;
  padding: 3px 10px; border-radius: 999px;
  background: var(--status-on); color: #fff;
  font-size: 12px; font-weight: 700;
}
.plan-run { cursor: pointer; font: inherit; color: inherit; }
.plan-run:hover { border-color: var(--text-secondary); }
.tr-entry-tools { display: flex; gap: 5px; flex: none; }

.tr-sets { margin-top: 4px; }
.tr-set { display: flex; align-items: center; gap: 7px; padding: 4px 0; border-bottom: 1px solid var(--grid); }
.tr-set:last-child { border-bottom: none; }
.tr-set-num { width: 16px; text-align: right; color: var(--text-muted); font-size: 12px; font-variant-numeric: tabular-nums; }
.tr-marker {
  flex: none; width: 26px; height: 26px; border-radius: 6px;
  border: 1px solid var(--border); background: transparent;
  color: var(--text-muted); font: inherit; font-size: 12px; cursor: pointer; padding: 0;
}
.tr-marker.on { color: var(--text-primary); border-color: var(--text-muted); font-weight: 650; }
.tr-input {
  width: 62px; padding: 6px 4px; border: 1px solid var(--border); border-radius: 7px;
  background: var(--page); color: var(--text-primary); font: inherit; font-size: 14px;
  font-variant-numeric: tabular-nums; text-align: center;
}
.tr-input:focus { outline: none; border-color: var(--text-secondary); }
.tr-unit { color: var(--text-muted); font-size: 12px; }
.tr-x { font-size: 13px; }
.tr-tick {
  margin-left: auto; flex: none; width: 36px; height: 30px; border-radius: 7px;
  border: 1px solid var(--border); background: transparent;
  color: var(--text-muted); font-size: 15px; cursor: pointer; padding: 0;
}
.tr-tick.on { background: var(--status-on); border-color: var(--status-on); color: #fff; }
.tr-set.done .tr-input { color: var(--text-secondary); }
.tr-set-del {
  flex: none; width: 24px; height: 24px; border: none; border-radius: 6px;
  background: transparent; color: var(--text-muted); font-size: 15px; cursor: pointer; padding: 0;
}
.tr-set-del:hover { color: var(--status-critical); }
.tr-add-set {
  border: none; background: transparent; color: var(--text-secondary);
  font: inherit; font-size: 12px; cursor: pointer; padding: 6px 0 1px;
}
.tr-add-set:hover { color: var(--text-primary); }

/* The Strong-style inline rest bar (Rindo, 2026-08-25): a second copy of
   the countdown right beneath the set just ticked, so no scrolling up
   mid-rest. The fill drains right-to-left as the rest runs out; at zero
   the bar flashes green exactly like the top one. The top bar stays. */
.tr-rest {
  position: relative; display: flex; align-items: center; gap: 8px;
  margin: 5px 0 3px; padding: 3px 10px; min-height: 30px;
  border: 1px solid var(--text-secondary); border-radius: 7px;
  background: var(--surface); overflow: hidden;
}
.tr-rest-fill {
  position: absolute; inset: 0; width: 100%;
  background: var(--band); transition: width 0.25s linear;
}
.tr-rest-txt {
  position: relative;
  font-variant-numeric: tabular-nums; font-size: 12.5px; font-weight: 600;
}
.tr-rest .btn { position: relative; }
.tr-rest.flash { border-color: var(--status-on); animation: tr-flash 0.9s ease-in-out 4; }
@media (prefers-reduced-motion: reduce) {
  .tr-rest.flash { animation: none; }
  .tr-rest-fill { transition: none; }
}
.tr-add-ex { margin: 2px 0 12px; }
.tr-actions { display: flex; gap: 8px; align-items: center; border-top: 1px solid var(--grid); padding-top: 12px; }
.tr-finish { font-weight: 650; }
.tr-empty p { margin: 0 0 10px; }
.tr-owed { margin-top: 10px; }

/* Exercise picker: swap and ad-hoc add share it. */
.tr-picker-back {
  position: fixed; inset: 0; z-index: 60; background: rgba(0, 0, 0, 0.45);
  display: flex; align-items: flex-start; justify-content: center;
  padding: 56px 14px 14px;
}
.tr-picker {
  width: 100%; max-width: 480px; max-height: 78vh; overflow: auto;
  background: var(--surface); border: 1px solid var(--border); border-radius: 12px;
  padding: 14px;
}
.tr-picker-title { margin: 0 0 10px; font-size: 15px; }
.tr-picker-q { width: 100%; text-align: left; margin-bottom: 8px; }
.tr-picker-list { display: flex; flex-direction: column; }
.tr-pick-row {
  display: flex; align-items: baseline; justify-content: space-between; gap: 10px;
  width: 100%; text-align: left; padding: 7px 4px;
  border: none; border-bottom: 1px solid var(--grid);
  background: transparent; color: var(--text-primary); font: inherit; cursor: pointer;
}
.tr-pick-row:hover { background: var(--band); }
.tr-pick-name { font-size: 13px; font-weight: 550; }
.tr-pick-muscles { color: var(--text-muted); font-size: 11px; text-align: right; flex: none; max-width: 46%; }
.tr-pick-custom .tr-pick-name { font-style: italic; }
.tr-pick-more, .tr-pick-note { font-size: 11px; margin: 8px 0 0; }

/* Settings (Rindo, 2026-08-23): the rest timer's seconds, per device (F2). */
.settings-pop {
  position: fixed; top: 64px; right: 16px; z-index: 70;
  width: min(320px, calc(100vw - 24px));
  background: var(--surface); border: 1px solid var(--border); border-radius: 12px;
  padding: 14px; box-shadow: 0 10px 30px rgba(0, 0, 0, 0.25);
}
.settings-pop h3 { margin: 0 0 10px; font-size: 14px; }
.settings-row { display: flex; align-items: center; justify-content: space-between; gap: 10px; font-size: 13px; }
.settings-input { display: flex; align-items: center; gap: 6px; color: var(--text-muted); font-size: 12px; }
.settings-input input {
  width: 64px; padding: 6px; border: 1px solid var(--border); border-radius: 7px;
  background: var(--page); color: var(--text-primary); font: inherit; text-align: center;
}
.settings-note { font-size: 11.5px; margin: 8px 0 12px; }


@media (max-width: 640px) {
  .lbl-long { display: none; }
  .lbl-short { display: inline; }

  /* Two whole-screen bounces hide behind a horizontal swipe, and the fix for
     each is one line here (Rindo's video + follow-up, 2026-08-01):
     1. The swipe tug's translateX past the right page edge counts as
        scrollable overflow — Chrome picked it up mid-gesture as real panning
        width (375 → 415px) and its overscroll stretch bounced the screen.
        `overflow-x: hidden` removes the *user's* ability to pan that width.
     2. Chrome's swipe-back navigation: a left-to-right drag on a page that
        can't scroll left pulls the whole viewport right and springs back
        (right-to-left was fine only because there is no forward history).
        `overscroll-behavior-x: none` disables it — but it is ONLY honoured
        on scroll containers, which is why this must be `hidden`, NOT `clip`:
        clip removes the scroll container and silently voids the guard.
        That was the first version of this fix, and it is why the back-swipe
        bounce survived it. On body too, for Chromes that still propagate
        overscroll-behavior from body to the viewport.
     Sticky bar, vertical scrolling and the drill-down table's own sideways
     scroll container are all unaffected. A swipe starting at the very screen
     edge is the OS back gesture — no page can override that one. */
  html {
    overflow-x: hidden;
    overscroll-behavior-x: none;
  }
  /* Third layer (Rindo, 2026-08-01, third report: a right-edge-to-left swipe
     still bumped — his phone is on three-button nav, so it wasn't the OS
     gesture). Chromium's newer edge-initiated history swipe and the Android
     overscroll stretch are decided at gesture level, and some builds run
     them even with overscroll-behavior set. `touch-action: pan-y pinch-zoom`
     removes the decision entirely: the browser owns vertical scrolling and
     pinch zoom, and a horizontal drag belongs to this page's own swipe
     handler — no browser pan, no overscroll, nothing left to bounce. Safe at
     phone width only because nothing here scrolls sideways — the drill-down
     table reflows into blocks instead (2026-07-28). Costs double-tap zoom
     (pinch still zooms). Desktop keeps native behaviour. */
  body {
    overscroll-behavior-x: none;
    touch-action: pan-y pinch-zoom;
  }

  /* Holiday scenery is a gutter feature and a phone has no gutters — the
     fixed layer would only ever peek out as odd slivers beside the cards.
     The plan panel's vignette is the phone's share of the flair
     (2026-08-14, same change as the desktop scenery). */
  body.holiday::before { display: none; }

  /* ---- chrome: three pinned lines, ~120px all told ---- */
  .page { padding: 12px 12px 28px; }

  /* The WHOLE top chrome is one sticky block — title bar, control row and
     the Status · Plan · Last workout bar (Rindo, 2026-08-02: "no matter how
     much I scroll, these three lines ought to be constant"; only the title
     bar had stuck since 2026-07-31). Full-bleed via negative margins so
     cards slide under it rather than showing past its sides in the page
     gutters. Metrics here and on the rows below are deliberately tight —
     same ask: "make those bars narrower … eke out as much real estate as
     you can". Tapping a tab already scrolls to the top (app.js), which is
     what makes switching from deep in a page land somewhere sensible. */
  .top-chrome {
    display: block;
    position: sticky; top: 0; z-index: 30;
    background: var(--page);
    margin: -12px -12px 10px; padding: 6px 12px;
    border-bottom: 1px solid var(--border);
  }

  /* Title and buttons share one line; the tagline steps aside on phones
     (its text is locked, not its visibility). No longer sticky itself —
     the wrapper above carries that now. */
  .header {
    position: static;
    align-items: center; gap: 8px;
    margin: 0 0 6px; padding: 0;
    border-bottom: none;
  }
  .header-titles { min-width: 0; }
  h1 {
    font-size: 16px; line-height: 1.2;
    white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
  }
  .tagline { display: none; }
  .header-actions { flex-wrap: nowrap; align-items: center; position: relative; }

  /* The hamburger and its dropdown (Rindo, 2026-07-31): Refresh, Upload and
     Sign out live behind one ☰ so the sticky bar stays a bar. Buttons get
     their full labels back down here — the icon-only compression was for the
     old inline row, not for a menu. */
  .menu-toggle { display: inline-flex; padding: 6px 10px; font-size: 16px; line-height: 1; }
  /* A phone needs no phone view — this also stops the 384px iframe offering
     one inside itself. */
  #btn-phone { display: none; }
  .header-menu {
    display: none; position: absolute; top: calc(100% + 6px); right: 0;
    flex-direction: column; align-items: stretch; gap: 6px; min-width: 176px;
    background: var(--surface); border: 1px solid var(--border);
    border-radius: 10px; padding: 8px;
    box-shadow: 0 8px 24px rgba(0, 0, 0, 0.18);
  }
  .header-menu.open { display: flex; }
  .header-menu .btn { text-align: left; }
  .header-menu .lbl-long { display: inline; }

  /* The drill-down scrolls into view on tap — keep it out from under the
     sticky chrome. 155px = the block at its tallest (a wrapped upload/mirror
     status line under the title); at its usual ~120px the extra margin just
     leaves a harmless gap. */
  #drilldown { scroll-margin-top: 155px; }
  /* ~138px of pinned chrome at 375px (see AGENTS.md) plus a little room. */
  #weeks-card, #bf-card { scroll-margin-top: 148px; }

  /* Keep "last workout", drop the export filename. Both status lines clamp
     to ONE line each (Rindo, 2026-08-02: the chrome is all pinned now, so
     every wrapped line costs scrolling room forever — the mirror line alone
     wrapped to three). The signal leads each string, so the ellipsis only
     eats filenames and counts. */
  .file-name { display: none; }
  #file-info { font-size: 11.5px; }
  .sync-line { font-size: 11px; }
  .subtitle { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }

  /* Every control on ONE line (Rindo, 2026-07-28: "cram them all into one
     line for mobile"). `nowrap` is the whole trick; everything else is the
     budget that makes it fit 375px — small padding, short labels, and the
     date as the only element allowed to shrink. */
  .filter-row { gap: 4px; margin-bottom: 6px; flex-wrap: nowrap; }
  .filter-row .btn, .filter-row .seg-btn {
    padding: 4px 7px; font-size: 11.5px; white-space: nowrap;
  }
  .filter-row .segmented, #btn-today, #btn-latest { flex: 0 0 auto; }
  .filter-row .btn-icon { padding: 4px 6px; }
  .week-nav { flex: 1 1 auto; justify-content: center; gap: 2px; min-width: 0; }
  .window-label {
    min-width: 0; font-size: 11.5px;
    overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  }

  /* ---- stat tiles: three across, not wrapped ---- */
  .stats-row { gap: 6px; margin-bottom: 10px; }
  .stat-tile { flex: 1 1 0; min-width: 0; padding: 6px 8px; }
  .stat-label { font-size: 10.5px; }
  .stat-value { font-size: 19px; }
  /* BF% (15 Sep) label (Rindo, 2026-09-26): one line, never wraps at 375px. */
  #tile-bf .stat-label { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

  /* Visitor phone: 2x2 rather than the owner's three-in-a-row squeeze — four
     tiles per section (Cool Stats, Last 7 days) read better that way at
     375px (Rindo, 2026-09-26 restructure). */
  body[data-access="visitor"] #body-stats-row:not([hidden]),
  body[data-access="visitor"] #activity-stats-row {
    display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 6px;
  }
  body[data-access="visitor"] #body-stats-row[hidden] { display: none; }
  body[data-access="visitor"] .public-desktop-tile { display: flex; }
  body[data-access="visitor"] .stat-tile { min-width: 0; }
  body[data-access="visitor"] .stat-reading { display: flex; align-items: baseline; flex-wrap: wrap; gap: 4px; }
  body[data-access="visitor"] .stat-delta { display: inline; font-size: 10px; color: var(--text-muted); white-space: nowrap; }
  body[data-access="visitor"] .stat-delta.up { color: var(--status-on); }
  body[data-access="visitor"] .stat-delta.down { color: var(--status-critical); }
  body[data-access="visitor"] #tile-bf .stat-caption { display: none; }
  body[data-access="visitor"] #tile-bf .stat-label { white-space: normal; }

  /* ---- cards: tighter, and in his order ---- */
  .card { padding: 12px 12px; margin-bottom: 10px; border-radius: 10px; }
  .card h2 { font-size: 13px; margin-bottom: 8px; }
  /* The long explainer notes are desktop-only reading. */
  #bars-card .h2-note, #gaps-note { display: none; }

  /* Rindo's 2026-08-23 rearrangement: phone order now just mirrors DOM
     order (the drill-down sits right below "Sets per muscle this week",
     the scorecard is appended at the bottom as the one-stop-shop finale) —
     kept explicit anyway, same as before, as the documented fallback if
     tabs ever go (AGENTS.md). */
  #dashboard { display: flex; flex-direction: column; }
  .stats-row   { order: 0; }
  .main-grid   { order: 1; }  /* body coverage, then sets per muscle */
  #drilldown   { order: 2; }  /* opens next to what you tapped */
  /* Visitor-only heading; owner never renders it, so its order elsewhere is
     moot. It has to sit ahead of #weeks-card explicitly — unlike the Cool
     Stats/Last 7 days headings, its default order (0) would put it at the
     very top, tied with .stats-row (Rindo, 2026-09-26 restructure). */
  #section-trends { order: 2; }
  #weeks-card  { order: 3; }  /* tap Workout days to jump here */
  #bf-card     { order: 4; }  /* tap Body fat to jump here */
  #plan-card   { order: 5; }
  #gaps-card   { order: 6; }
  #scorecard-card { order: 7; }  /* moved to the bottom: one-stop shop */
  #unmapped-card { display: none; }  /* mapping-debug card, not gym reading */
  #tracker-card { order: 8; }
  .main-grid { gap: 10px; }

  /* ---- workouts per week: twelve columns must fit 320px ----
     Columns are flex:1 1 0 with min-width:0 (base rule), so they can't
     force sideways overflow on their own; this just keeps the blocks and
     gaps proportionate at phone size (12 columns, 11 gaps at 4px = 44px,
     leaving ~19px per column inside a 320px screen — plenty for a 12px
     block). */
  .wk-chart { gap: 4px; padding-top: 8px; --wk-block: 12px; }
  .wk-label { font-size: 9px; }

  /* ---- tabs ----
     The bar, the slide-in cue and the per-tab visibility all moved to the
     global "tabs: both widths" section on 2026-08-02, when desktop got the
     tabs too (Rindo). Only phone-specific tab tweaks may live here. */

  /* Inside the sticky chrome: the wrapper's own padding is the gap below,
     and the bar sheds a few px of height (Rindo, 2026-08-02: narrower). */
  .tabs { margin-bottom: 0; padding: 2px; }
  .tab { padding: 4px; font-size: 12px; }

  /* ---- sets per muscle: 20 rows, halve the row height ----
     The value column has to be wide enough for "12 / 12–20" on ONE line.
     Too narrow and it wraps, which sets the row height for the whole chart —
     that alone was the difference between a 737px and a 430px table. */
  .bar-row, .bar-axis {
    grid-template-columns: 76px 1fr 62px 14px;
    gap: 5px; padding: 1px 2px;
  }
  .bar-row { min-height: 22px; }
  .bar-label-text { font-size: 11.5px; }
  .bar-label-priority { font-size: 9px; line-height: 1.1; }
  .bar-track { height: 15px; }
  .bar-value { white-space: nowrap; font-size: 11px; }
  .bar-value-num { font-size: 12px; }
  /* Inline, not block: on its own line it doubles every row in the chart. */
  .bar-value-target { display: inline; font-size: 10.5px; }
  /* Keep the coloured status dot, drop the word beside it: "Far behind" is
     already said by the dot's colour and by where the bar sits against its
     goal band. The legend keeps its labels, or the icons stop being decodable. */
  .chip-label { display: none; }
  .legend .chip-label { display: inline; }

  /* Legend: all five on ONE line (Rindo, 2026-07-28 — "I don't mind if that
     font is smaller"). It was two lines and 45px tall. Five labels this wordy
     don't fit any fixed size across 320–430px screens, so the type is sized
     off the viewport and the dots scale with it (em, not px) — the fixed part
     is only the gaps. Capped at 10.5px so big phones don't get a huge legend. */
  .legend {
    flex-wrap: nowrap; gap: 0 3px; justify-content: space-between; margin-top: 8px;
  }
  .legend .chip { font-size: min(10.5px, calc(3.15vw - 2.4px)); gap: 2px; }
  .legend .chip-dot { width: 1.2em; height: 1.2em; font-size: 0.78em; }

  /* ---- drill-down: no sideways scrolling ----
     Six columns can't fit a phone, and Rindo won't scroll sideways for them
     (2026-07-28). Same markup, re-laid out by CSS alone — the table becomes
     one block per exercise: name on top, then "main · 4 sets · 1.0 each ·
     counts as 4", then the days. The row labels the dropped <thead> used to
     carry come back as ::before/::after text. */
  .drill-table { min-width: 0; width: 100%; display: block; }
  .drill-table thead { display: none; }
  .drill-table tbody, .drill-table tr, .drill-table td { display: block; }
  .drill-table tr {
    display: flex; flex-wrap: wrap; gap: 0 5px;
    padding: 6px 0; border-bottom: 1px solid var(--grid);
  }
  .drill-table tr:last-child { border-bottom: none; }
  .drill-table td {
    padding: 0; border: none; font-size: 11.5px; color: var(--text-secondary);
  }
  .drill-table .num { text-align: left; }
  .drill-table .drill-exercise {
    flex: 1 1 100%; font-size: 12.5px; font-weight: 600; color: var(--text-primary);
  }
  .drill-table td:nth-child(3)::before,
  .drill-table td:nth-child(4)::before,
  .drill-table td:nth-child(5)::before { content: "· "; color: var(--text-muted); }
  .drill-table td:nth-child(3)::after { content: " sets"; color: var(--text-muted); }
  .drill-table td:nth-child(4)::after { content: " each"; color: var(--text-muted); }
  .drill-table td:nth-child(5)::before { content: "· counts as "; color: var(--text-muted); }
  .drill-table .drill-days { flex: 1 1 100%; font-size: 11px; color: var(--text-muted); }

  /* The last axis tick sits at 100% and spills past the track. */
  .bar-axis-track { overflow: hidden; }

  /* ---- this week's plan ---- */
  /* The holiday pill shares the title line rather than taking one of its own
     (Rindo, 2026-07-29: the timing pill under Push·Pull·Legs·Arms "is just
     weird" — the 60/90 pills lived here too until the budget stopped being a
     choice, 2026-08-06). `order` is what puts it there — in DOM order it
     comes after the "why" spans, and those are forced to full width right
     below. */
  .today-head { gap: 2px 8px; align-items: center; }
  .today-title { font-size: 10.5px; order: 1; }
  .today-mode { order: 2; }
  .today-mode .seg-btn { padding: 3px 7px; font-size: 11px; }
  .today-why { font-size: 11px; flex-basis: 100%; order: 3; }
  .today-controls { gap: 5px; margin: 6px 0 2px; }
  .today-controls .seg-btn { padding: 4px 7px; font-size: 11.5px; }
  /* Five rotation types + Core no longer fit one row of the segmented
     control at 375px (Rindo, 2026-09-26: Upper C). The box was sized to its
     content with no wrap, so the extra pill silently ran past the edge;
     forcing it to the panel's full width lets its own buttons wrap instead. */
  .today-controls .segmented { flex: 1 1 100%; flex-wrap: wrap; }
  /* Exercise name on its own line, then scheme + last weight under it. */
  .today-row { gap: 1px 8px; padding: 4px 0; flex-wrap: wrap; }
  .today-ex { flex: 1 1 100%; }
  .today-name { font-size: 12.5px; }
  .today-note { font-size: 11px; }
  .today-scheme, .today-weight { font-size: 11.5px; }
  .today-foot { font-size: 11px; }

  /* Live tracker at thumb size (2026-08-23). 16px inputs stop mobile
     browsers zooming the page when a weight box takes focus. */
  /* today-ex takes the row's full width above, so scheme/weight/add wrap
     onto their own line together; margin-left: auto pins + at the right
     edge there too, matching the desktop row (Rindo, 2026-09-26). */
  .today-add { width: 28px; height: 28px; font-size: 16px; margin-left: auto; }
  .tr-input, .settings-input input { font-size: 16px; }
  .tr-tick { width: 40px; height: 34px; }
  .tr-marker { width: 30px; height: 30px; }
  .plan-row { padding: 6px 0; gap: 8px; flex-wrap: wrap; }
  .plan-row-label { flex-basis: 100%; }
  .plan-subnote { margin-left: 0; font-size: 11px; }
  /* Push · Pull · Legs · Arms on ONE line (Rindo, 2026-07-28) — they wrapped
     after three. Same viewport-scaled trick as the legend, so it holds from
     320px up. Wrapping is deliberately still allowed: a week where a session
     was logged twice carries a longer note ("✓ Mon, Thu"), and a second line
     is a better failure than pills running off the card. */
  .plan-items { gap: 2px; }
  .plan-item { font-size: min(11.5px, 2.95vw); padding: 2px 5px; gap: 2px; }
  .plan-item-note { font-size: 0.88em; }

  /* ---- last workout: two lines per exercise, not four ----
     The pre-existing rule below gives the name its own line; without these
     the three metrics each took a line of their own too, at 59px a row
     across 12 exercises. Name on top, the numbers sharing one line under it. */
  .score-row { gap: 0 10px; padding: 3px 0; }
  .score-name { font-size: 12.5px; line-height: 1.3; }
  .score-sets, .score-top, .score-delta { font-size: 11px; flex: 0 1 auto; min-width: 0; }
  .score-summary { gap: 8px; }
  .score-summary .stat-value, .score-summary .score-big { font-size: 17px; }
  .score-compare { font-size: 11px; }
  .plan-score-row { font-size: 11.5px; padding: 2px 0; }
  .plan-score-state { font-size: 11px; }
  .plan-score-verdict { font-size: 11px; }
  .plan-score-verdict-label { padding: 5px 12px; }
  .plan-score-verdict-ghost { padding: 5px 0; }
  .plan-score-verdict-word { padding: 0 8px; }

  /* ---- collapsible cards ---- */
  .card-collapsible > h2 {
    cursor: pointer; user-select: none;
    display: flex; align-items: center; gap: 6px; margin-bottom: 0;
  }
  .card-collapsible:not(.collapsed) > h2 { margin-bottom: 8px; }
  .card-caret { display: inline-block; transition: transform 0.15s; font-size: 11px; }
  .card-collapsible.collapsed .card-caret { transform: rotate(-90deg); }
}

/* ---------- tooltip ---------- */

.tooltip {
  position: absolute; z-index: 10; pointer-events: none;
  background: var(--surface); border: 1px solid var(--border); border-radius: 8px;
  box-shadow: 0 4px 14px rgba(0, 0, 0, 0.14);
  padding: 8px 12px; max-width: 300px; font-size: 12.5px;
}
.tip-head { display: flex; align-items: baseline; gap: 8px; }
.tip-value { font-weight: 700; font-size: 14px; }
.tip-group { color: var(--text-secondary); }
.tip-status { margin: 2px 0 6px; font-size: 12px; color: var(--text-secondary); }
.tip-line { display: flex; justify-content: space-between; gap: 14px; }
.tip-line-name { color: var(--text-secondary); }
.tip-line-val { font-variant-numeric: tabular-nums; }
.tip-more { color: var(--text-muted); margin-top: 3px; font-size: 11.5px; }


/* The old 560px block was merged into the 640px phone block above on
   2026-07-28. It sat *after* that block in the file, so at 375px it quietly
   won every overlap — its `min-height: 34px` and `display: block` on the goal
   range were why the chart stayed ~750px tall after being "compressed".
   One phone breakpoint now; don't reintroduce a second one. */

/* Sign-off footer (2026-09-27), as on Rindo's other projects. */
.site-footer {
  margin-top: 32px;
  padding: 16px 24px 24px;
  border-top: 1px solid var(--border);
  text-align: center;
  font-size: 12px;
  color: var(--text-muted);
}
.site-footer a {
  color: inherit;
  text-decoration: underline;
  text-underline-offset: 2px;
}
.site-footer a:hover { color: var(--text-primary); }
