/* ============================================================
   metamentum App — app.css
   Prototype (Iteration 0): static shell, no build step.
   Tokens mirrored from site/css/style.css ("Vitality Steel")
   so the app reads as the same product as the marketing site.
   ============================================================ */

/* Fonts are served from this site (static/fonts/, latin subset): no request to a
   third-party host on the way to first paint, which on a weak gym connection is
   the difference between text now and text in a second or two. Plus Jakarta Sans
   (headings, buttons) and Lora (body) are variable fonts, so every weight in the
   range comes from one file; Ubuntu is only the brand name, bold. OFL / Ubuntu Font
   License — see static/fonts/README.txt. */
@font-face { font-family: 'Lora'; font-style: italic; font-weight: 400 600; font-display: swap; src: url("../fonts/lora-italic-400-600.1640b0b69ef0.woff2") format('woff2'); unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; }
@font-face { font-family: 'Lora'; font-style: normal; font-weight: 400 600; font-display: swap; src: url("../fonts/lora-normal-400-600.9577af3ccda5.woff2") format('woff2'); unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; }
@font-face { font-family: 'Plus Jakarta Sans'; font-style: normal; font-weight: 300 800; font-display: swap; src: url("../fonts/plus-jakarta-sans-normal-300-800.7660bd9909fb.woff2") format('woff2'); unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; }
@font-face { font-family: 'Ubuntu'; font-style: normal; font-weight: 700; font-display: swap; src: url("../fonts/ubuntu-normal-700.9418887ae687.woff2") format('woff2'); unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; }

:root {
  /* Color palette — same as site/css/style.css */
  --c-dark:       #1B3A4B;
  --c-dark-2:     #162F3D;
  --c-brand:      #2C6E8A;
  --c-brand-lt:   #4A90A8;
  --c-accent:     #D97706;
  --c-accent-lt:  #F59E0B;
  --c-accent-dim: rgba(217,119,6,0.12);
  --c-bg:         #F9F8F5;
  --c-surface:    #FFFFFF;
  --c-ice:        #E0EEF4;
  --c-ice-2:      #EBF4F8;
  --c-text:       #1C1917;
  --c-text-md:    #44403C;
  --c-text-lt:    #78716C;
  --c-border:     #E7E5E1;
  --c-border-md:  #D6D3CE;

  /* Semantic (app-only) */
  --c-good:      #2F8F5B;
  --c-good-lt:   #4DB37E;
  --c-good-dim:  rgba(47,143,91,0.12);
  --c-warn:      #D97706;
  --c-warn-dim:  rgba(217,119,6,0.12);
  --c-bad:       #C4432E;
  --c-bad-lt:    #E2654A;
  --c-bad-dim:   rgba(196,67,46,0.12);

  --font-display: 'Plus Jakarta Sans', system-ui, -apple-system, sans-serif;
  --font-body:    'Lora', Georgia, 'Times New Roman', serif;
  --font-brand:   'Ubuntu', 'Plus Jakarta Sans', system-ui, -apple-system, sans-serif;

  --text-xs:   0.75rem;
  --text-sm:   0.875rem;
  --text-base: 1rem;
  --text-lg:   1.125rem;
  --text-xl:   1.3125rem;
  --text-2xl:  1.625rem;
  --text-3xl:  2rem;

  --sp-1: 0.25rem; --sp-2: 0.5rem; --sp-3: 0.75rem; --sp-4: 1rem;
  --sp-5: 1.25rem; --sp-6: 1.5rem; --sp-8: 2rem; --sp-10: 2.5rem; --sp-12: 3rem;

  --r-sm: 4px; --r-md: 8px; --r-lg: 14px; --r-xl: 20px; --r-full: 9999px;

  --shadow-xs: 0 1px 2px rgba(0,0,0,.06);
  --shadow-sm: 0 1px 4px rgba(0,0,0,.07), 0 1px 2px rgba(0,0,0,.05);
  --shadow-md: 0 4px 12px rgba(0,0,0,.08), 0 2px 4px rgba(0,0,0,.05);
  --shadow-lg: 0 10px 30px rgba(0,0,0,.10), 0 4px 10px rgba(0,0,0,.06);

  --t-fast: 0.15s ease;
  --t-base: 0.25s ease;

  --sidebar-w: 240px;
  --topbar-h: 64px;
}

/* ---- Reset ---- */
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
html { -webkit-text-size-adjust: 100%; }
/* Without !important, an element's own inline `style="display:..."` (e.g.
   a card that's also flex/grid for its content) beats the [hidden]
   attribute's default UA-stylesheet rule — the element stays visible even
   though `hidden` is set. Toggling visibility everywhere in this app is
   done via the `hidden` attribute/property (never style.display), so this
   one rule is what actually makes that reliable. */
[hidden] { display: none !important; }
body {
  font-family: var(--font-body);
  font-size: var(--text-base);
  line-height: 1.6;
  color: var(--c-text);
  background: var(--c-bg);
  -webkit-font-smoothing: antialiased;
}
img, svg { max-width: 100%; display: block; }
a { color: inherit; text-decoration: none; }
ul, ol { list-style: none; }
button { cursor: pointer; border: none; background: none; font-family: inherit; font-size: inherit; color: inherit; }
input, textarea, select { font-family: inherit; font-size: inherit; color: inherit; }
h1, h2, h3, h4 { font-family: var(--font-display); color: var(--c-dark); font-weight: 700; line-height: 1.25; letter-spacing: -0.01em; }
h1 { font-size: var(--text-3xl); font-weight: 800; }
h2 { font-size: var(--text-2xl); }
h3 { font-size: var(--text-lg); }
h4 { font-size: var(--text-base); }
p { margin-bottom: var(--sp-3); color: var(--c-text-md); }
::selection { background: var(--c-accent-dim); }

/* ---- Prototype banner ---- */
.proto-banner {
  background: var(--c-dark);
  color: #fff;
  font-family: var(--font-display);
  font-size: var(--text-xs);
  text-align: center;
  padding: var(--sp-2) var(--sp-4);
  letter-spacing: 0.02em;
}
.proto-banner strong { color: var(--c-accent-lt); }

/* ---- Auth (login/onboarding) layout ---- */
.auth-shell {
  min-height: calc(100vh - 34px);
  display: flex;
  align-items: center;
  justify-content: center;
  padding: var(--sp-8) var(--sp-4);
  background:
    radial-gradient(1200px 600px at 15% -10%, var(--c-ice) 0%, transparent 60%),
    var(--c-bg);
}
.auth-card {
  width: 100%;
  max-width: 440px;
  background: var(--c-surface);
  border: 1px solid var(--c-border);
  border-radius: var(--r-xl);
  box-shadow: var(--shadow-lg);
  padding: var(--sp-8);
}
.auth-logo {
  display: flex;
  align-items: center;
  gap: var(--sp-2);
  justify-content: center;
  margin-bottom: var(--sp-6);
  font-family: var(--font-brand);
  font-weight: 700;
  font-size: var(--text-xl);
  color: var(--c-dark);
}
.auth-logo .dot { width: 10px; height: 10px; border-radius: 50%; background: var(--c-accent); }
.auth-tabs { display: flex; border: 1px solid var(--c-border); border-radius: var(--r-full); padding: 3px; margin-bottom: var(--sp-6); }
.auth-tabs button { flex: 1; text-align: center; padding: var(--sp-2) var(--sp-3); border-radius: var(--r-full); font-family: var(--font-display); font-size: var(--text-sm); font-weight: 600; color: var(--c-text-lt); transition: var(--t-fast); }
.auth-tabs button.is-active { background: var(--c-dark); color: #fff; }
.auth-panel { display: none; }
.auth-panel.is-active { display: block; }
.auth-card--wide { max-width: 640px; }
/* Signup's two plan buttons sit side by side, then stack on a phone. */
.plan-actions { display: flex; gap: var(--sp-3); }
@media (max-width: 640px) { .plan-actions { flex-direction: column; } }
/* Signup breadcrumb: every step, current one underlined like .tabs, finished ones muted-dark. */
.signup-steps { list-style: none; display: flex; justify-content: center; flex-wrap: wrap; gap: var(--sp-2); padding: 0; margin: 0 0 var(--sp-6); border-bottom: 1px solid var(--c-border); }
.signup-steps li { padding: var(--sp-3) var(--sp-4); font-family: var(--font-display); font-weight: 600; font-size: var(--text-sm); color: var(--c-text-lt); border-bottom: 2px solid transparent; margin-bottom: -1px; }
.signup-steps li.is-done { color: var(--c-text-md); }
.signup-steps li.is-active { color: var(--c-dark); border-color: var(--c-accent); }
.auth-foot { text-align: center; margin-top: var(--sp-5); font-size: var(--text-sm); color: var(--c-text-lt); }
.auth-foot a { color: var(--c-brand); font-weight: 600; }

/* An inline text link within a sentence — distinct from .btn, which is for
   a standalone action. Without this, a plain <a> inherits body text color
   (see the base `a` rule) and reads as unclickable — a real, previously
   unfixed gap (my_workouts.html's "Find a workout" empty-state link had
   it too), not just a Freestyle Session issue. */
.link { color: var(--c-brand); font-weight: 600; }
.link:hover { text-decoration: underline; }

/* ---- Form elements ---- */
.field { margin-bottom: var(--sp-4); }
.field label { display: block; font-family: var(--font-display); font-size: var(--text-sm); font-weight: 600; color: var(--c-dark); margin-bottom: var(--sp-2); }
.field .hint, .field-group .hint { font-size: var(--text-xs); color: var(--c-text-lt); margin-top: var(--sp-1); }
/* A <fieldset> of checkboxes that reads as one field ("What do you record for each set?"). */
.field-group { border: none; padding: 0; margin: 0 0 var(--sp-4); }
.field-group legend { font-family: var(--font-display); font-size: var(--text-sm); font-weight: 600; color: var(--c-dark); padding: 0; margin-bottom: var(--sp-2); }
.field-group .check-row { margin-bottom: var(--sp-2); }
.field-row { display: grid; grid-template-columns: 1fr 1fr; gap: var(--sp-4); }
.field-error { color: var(--c-bad); font-size: var(--text-xs); margin-top: var(--sp-1); }
.auth-footnote { margin-top: var(--sp-5); font-size: var(--text-sm); color: var(--c-text-lt); text-align: center; }
input[type="text"], input[type="email"], input[type="password"], input[type="number"], input[type="date"], input[type="datetime-local"], input[type="search"], textarea, select {
  width: 100%;
  padding: var(--sp-3) var(--sp-4);
  border: 1px solid var(--c-border-md);
  border-radius: var(--r-md);
  background: var(--c-surface);
  transition: var(--t-fast);
}
/* A button that looks like an empty field and opens a picker — the blank
   "Exercise" row on Freestyle Session and the builder's session page. */
.picker-field { display: block; font: inherit; width: 100%; padding: var(--sp-3) var(--sp-4); border: 1px solid var(--c-border-md); border-radius: var(--r-md); background: var(--c-surface); color: var(--c-text-lt); text-align: left; cursor: pointer; transition: var(--t-fast); }
.picker-field:hover { border-color: var(--c-brand); }
.picker-field:focus-visible { outline: none; border-color: var(--c-brand); box-shadow: 0 0 0 3px rgba(44,110,138,0.15); }
input:focus, textarea:focus, select:focus { outline: none; border-color: var(--c-brand); box-shadow: 0 0 0 3px rgba(44,110,138,0.15); }
input[aria-invalid="true"] { border-color: var(--c-bad); }
textarea { resize: vertical; min-height: 90px; }
.check-row { display: flex; align-items: flex-start; gap: var(--sp-2); font-size: var(--text-sm); color: var(--c-text-md); }
.check-row input { width: auto; margin-top: 3px; }
/* A checkbox's own label: plain-weight prose, not .field label's bold heading
   style (the longer selector is what wins over `.field label`). */
.field .check-label { margin-bottom: 0; font-weight: 400; color: var(--c-text-md); }
/* A <button> that reads as an inline link — opens a modal from inside a
   sentence (the legal documents beside the consent checkboxes). */
.link-button { display: inline; padding: 0; border: 0; background: none; font: inherit; font-weight: 600; color: var(--c-brand); cursor: pointer; text-decoration: underline; }

.toggle { display: block; position: relative; width: 42px; height: 24px; padding: 0; border-radius: var(--r-full); background: var(--c-border-md); flex-shrink: 0; transition: var(--t-fast); }
.toggle::after { content: ""; position: absolute; top: 3px; left: 3px; width: 18px; height: 18px; border-radius: 50%; background: #fff; transition: var(--t-fast); box-shadow: var(--shadow-xs); }
.toggle.is-on { background: var(--c-good); }
.toggle.is-on::after { left: 21px; }
.toggle:focus-visible { outline: 2px solid var(--c-brand); outline-offset: 2px; }
.toggle:disabled { opacity: 0.45; cursor: not-allowed; }

/* ---- Buttons ---- */
/* [hidden] loses to any class setting its own `display` at equal CSS
   specificity (author rules beat the UA default) — Session Logging and
   Freestyle Session's dynamic set-row/exercise-block JS (app.js) toggle `hidden`
   on real .btn elements, so this has to actually win. */
[hidden] { display: none !important; }
.btn { display: inline-flex; align-items: center; justify-content: center; gap: var(--sp-2); font-family: var(--font-display); font-weight: 700; font-size: var(--text-sm); padding: var(--sp-3) var(--sp-5); border-radius: var(--r-md); transition: var(--t-fast); white-space: nowrap; }
.btn--primary { background: var(--c-accent); color: #fff; }
.btn--primary:hover { background: var(--c-accent-lt); }
.btn--dark { background: var(--c-dark); color: #fff; }
.btn--dark:hover { background: var(--c-dark-2); }
.btn--outline { background: transparent; border: 1.5px solid var(--c-border-md); color: var(--c-dark); }
.btn--outline.is-active { background: var(--c-ice); border-color: var(--c-brand); color: var(--c-brand); }
.btn--outline:hover { border-color: var(--c-brand); color: var(--c-brand); }
/* Ghost still needs to read as a button, not as plain colored text — a
   fully transparent, borderless .btn--ghost was visually indistinguishable
   from a .link (no box at all), which is exactly the "is this a button?"
   confusion it should never cause. A lighter border than .btn--outline's
   keeps the intended quieter/secondary weight (Stop Tracking, Remove,
   Cancel Goal, etc. next to a bolder primary/outline action) while still
   giving it a real, consistent clickable-looking edge. */
.btn--ghost { background: transparent; border: 1px solid var(--c-border); color: var(--c-brand); }
.btn--ghost:hover { border-color: var(--c-brand); background: var(--c-ice); }
.btn--sm { padding: var(--sp-2) var(--sp-4); font-size: var(--text-xs); }
.btn--block { width: 100%; }
.btn--danger { background: transparent; border: 1px solid var(--c-border); color: var(--c-bad); }
.btn--danger:hover { border-color: var(--c-bad); background: var(--c-bad-dim); }
.btn:disabled { opacity: 0.45; cursor: not-allowed; }

/* Daily-status face picker (onboarding) — dedicated active style, not a
   reused .btn variant, so cascade order between .btn--outline/.btn--dark
   can't silently make the "selected" state invisible. */
.face-btn.is-active-face { background: var(--c-accent); border-color: var(--c-accent); }

/* ---- App shell: sidebar + topbar ---- */
.app-shell { display: flex; min-height: calc(100vh - 34px); }

.app-sidebar {
  width: var(--sidebar-w);
  flex-shrink: 0;
  background: var(--c-dark);
  color: #fff;
  display: flex;
  flex-direction: column;
  position: sticky;
  top: 0;
  height: 100vh;
}
.app-sidebar .side-logo { display: flex; align-items: center; gap: var(--sp-2); padding: var(--sp-5) var(--sp-5) var(--sp-4); font-family: var(--font-brand); font-weight: 700; font-size: var(--text-lg); }
.app-sidebar .side-logo .dot { width: 8px; height: 8px; border-radius: 50%; background: var(--c-accent); }
.side-nav { flex: 1; padding: var(--sp-2) var(--sp-3); }
.side-nav a { display: flex; align-items: center; gap: var(--sp-3); padding: var(--sp-3) var(--sp-3); border-radius: var(--r-md); font-family: var(--font-display); font-size: var(--text-sm); font-weight: 600; color: rgba(255,255,255,0.72); margin-bottom: var(--sp-1); transition: var(--t-fast); }
.side-nav a .ic { width: 18px; height: 18px; flex-shrink: 0; }
.side-nav a:hover { background: rgba(255,255,255,0.06); color: #fff; }
.side-nav a.is-active { background: var(--c-accent); color: #fff; }

.app-main { flex: 1; min-width: 0; display: flex; flex-direction: column; }
.app-topbar {
  height: var(--topbar-h);
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 0 var(--sp-6);
  background: var(--c-surface);
  border-bottom: 1px solid var(--c-border);
  position: sticky;
  top: 0;
  z-index: 5;
}
.app-topbar h1 { font-size: var(--text-lg); }
.topbar-title { display: flex; align-items: center; gap: var(--sp-3); }
.topbar-actions { display: flex; align-items: center; gap: var(--sp-4); }
.topbar-actions form { display: contents; }
.avatar { border: 0; padding: 0; cursor: pointer; width: 34px; height: 34px; border-radius: 50%; background: var(--c-ice); color: var(--c-brand); display: flex; align-items: center; justify-content: center; font-family: var(--font-display); font-weight: 700; font-size: var(--text-sm); }
/* The avatar's dropdown (Upgrade to Client for a free User, Profile, Log Out) — app.js [data-user-menu]. */
.user-menu { position: relative; }
.user-menu-list { position: absolute; right: 0; top: calc(100% + var(--sp-2)); z-index: 30; min-width: 200px; padding: var(--sp-2); background: #fff; border: 1px solid var(--c-border); border-radius: var(--r-lg); box-shadow: var(--shadow-lg); }
.user-menu-who { padding: var(--sp-2) var(--sp-3); margin-bottom: var(--sp-1); border-bottom: 1px solid var(--c-border); font-size: var(--text-xs); color: var(--c-text-lt); overflow-wrap: anywhere; }
.user-menu-list a, .user-menu-list button { display: block; width: 100%; padding: var(--sp-3); border: 0; border-radius: var(--r-md); background: none; font-family: var(--font-display); font-size: var(--text-sm); font-weight: 600; color: var(--c-dark); text-align: left; text-decoration: none; cursor: pointer; }
.user-menu-list a:hover, .user-menu-list button:hover { background: var(--c-ice); }
.user-menu-list a.user-menu-upgrade { margin-bottom: var(--sp-1); border-bottom: 1px solid var(--c-border); border-radius: var(--r-md) var(--r-md) 0 0; }
.topbar-actions form.user-menu-role { display: block; padding: var(--sp-2) var(--sp-3); margin-bottom: var(--sp-1); border-bottom: 1px solid var(--c-border); }
.user-menu-role label { display: block; margin-bottom: var(--sp-1); font-size: var(--text-xs); color: var(--c-text-lt); }
.nav-burger { display: none; }

.role-switch {
  font-family: var(--font-display);
  font-weight: 600;
  font-size: var(--text-xs);
  padding: var(--sp-2) var(--sp-3);
  border: 1px solid var(--c-border-md);
  border-radius: var(--r-full);
  background: var(--c-ice-2);
  color: var(--c-dark);
  width: 100%;
}

.app-content { padding: var(--sp-6); max-width: 1180px; width: 100%; margin: 0 auto; }

/* ---- Spacing utilities (new-code only — see prototype/README.md's
   inline-style convention, which stays as-is; this app never uses
   style= attributes going forward) ---- */
.session-list { list-style: none; padding: 0 0 0 var(--sp-4); margin: 0; font-size: var(--text-sm); color: var(--c-text-md); }
.session-list li { margin-bottom: var(--sp-1); }
.session-list a { margin-left: var(--sp-2); }
/* Real gap found 2026-09-25: only mb-4/mb-5 were ever defined here, even
   though mb-0/1/2/3/6 were already in use across templates (Goals,
   Analysis, Home, Profile, ...) — those classes were silently no-ops,
   falling back to each element's own default margin. Filling in the rest
   of the scale, matching the existing --sp-* tokens exactly. */
.mb-0 { margin-bottom: 0; }
.mb-1 { margin-bottom: var(--sp-1); }
.mb-2 { margin-bottom: var(--sp-2); }
.mb-3 { margin-bottom: var(--sp-3); }
.mb-4 { margin-bottom: var(--sp-4); }
.mb-5 { margin-bottom: var(--sp-5); }
.mb-6 { margin-bottom: var(--sp-6); }
.mt-0 { margin-top: 0; }
.p-0 { padding: 0; }
.mt-1 { margin-top: var(--sp-1); }
.mt-2 { margin-top: var(--sp-2); }
.mt-3 { margin-top: var(--sp-3); }
.mt-4 { margin-top: var(--sp-4); }
.mt-5 { margin-top: var(--sp-5); }
.mt-6 { margin-top: var(--sp-6); }
/* Pulls a subtitle back up under a .section-title's own bottom margin —
   the two "-N" variants match the two gaps actually used (Goals/Vision). */
.mt-pull-2 { margin-top: calc(-1 * var(--sp-2)); }
.mt-pull-3 { margin-top: calc(-1 * var(--sp-3)); }
.max-w-480 { max-width: 480px; }
.max-w-560 { max-width: 560px; }
.max-w-640 { max-width: 640px; }
.fw-400 { font-weight: 400; }
.block { display: block; }
.text-right { text-align: right; }
.text-center { text-align: center; }
/* The '*' next to a required field's label. */
.required-mark { }

/* ---- Django messages ---- */
.messages { list-style: none; padding: 0; margin: 0 0 var(--sp-5); display: flex; flex-direction: column; gap: var(--sp-2); }
.message { padding: var(--sp-3) var(--sp-4); border-radius: var(--r-md); font-size: var(--text-sm); border: 1px solid transparent; }
.message--success { background: var(--c-good-dim); color: var(--c-good); border-color: var(--c-good); }
.message--info { background: var(--c-ice-2); color: var(--c-brand); border-color: var(--c-brand-lt); }
/* A message with an action beside it (the verify-email banner): the button wraps under the text on a phone. */
.message--action { display: flex; align-items: center; justify-content: space-between; flex-wrap: wrap; gap: var(--sp-3); }
.message--action > span { flex: 1 1 14rem; }
.message-actions { display: flex; align-items: center; gap: var(--sp-2); }
/* ✕ on a banner that can be dismissed (app.js [data-dismissible]); 40px so a thumb can hit it. */
.message-dismiss { width: 40px; height: 40px; background: none; border: none; font-size: var(--text-base); color: inherit; opacity: 0.7; cursor: pointer; border-radius: var(--r-md); }
.message-dismiss:hover { opacity: 1; }
.message--warning { background: var(--c-warn-dim); color: var(--c-warn); border-color: var(--c-warn); }
.message--error { background: var(--c-bad-dim); color: var(--c-bad); border-color: var(--c-bad); }

/* ---- Cards / grid ---- */
.grid { display: grid; gap: var(--sp-5); }
.grid--2 { grid-template-columns: 1fr 1fr; }
.grid--3 { grid-template-columns: repeat(3, 1fr); }

.card { background: var(--c-surface); border: 1px solid var(--c-border); border-radius: var(--r-lg); padding: var(--sp-5); box-shadow: var(--shadow-sm); }
.card-head { display: flex; align-items: center; justify-content: space-between; margin-bottom: var(--sp-4); }
.card-head h3 { margin: 0; }
.card-head .btn { margin: 0; }
/* A dashed, "nothing here yet" card — same shape wherever a section on
   Home has nothing pinned/in progress (Goals/Analysis/Workouts). */
.empty-state { display: flex; flex-direction: column; align-items: center; justify-content: center; gap: var(--sp-2); text-align: center; }
/* Its own dashed/tinted card — used where there's no surrounding .card
   already providing a boundary (Home's Goals/Analysis sections). */
.empty-state--boxed { background: var(--c-ice-2); border-style: dashed; min-height: 120px; }
/* Just vertical breathing room — used inside a .card that already has its
   own border (Home's "Workouts in progress"). */
.empty-state--inline { padding: var(--sp-4) 0; }
/* Taller variant — Analysis's "nothing to chart yet" card needs more
   room than Home's compact empty states. */
.empty-state--boxed-lg { min-height: 200px; }
.empty-state-icon { font-size: 1.5rem; }
/* A past/read-only record — same tint as .empty-state--boxed, just not
   dashed and not necessarily empty (e.g. a completed/cancelled goal). */
.card--muted { background: var(--c-ice-2); }

/* ---- Badges / pills ---- */
.badge { display: inline-flex; align-items: center; gap: var(--sp-1); font-family: var(--font-display); font-size: var(--text-xs); font-weight: 700; padding: 3px var(--sp-2); border-radius: var(--r-full); }
.badge--good { background: var(--c-good-dim); color: var(--c-good); }
.badge--warn { background: var(--c-warn-dim); color: var(--c-warn); }
.badge--bad  { background: var(--c-bad-dim); color: var(--c-bad); }
.badge--dim  { background: var(--c-ice); color: var(--c-brand); }
.badge--outline { border: 1px solid var(--c-border-md); color: var(--c-text-lt); }

/* ---- Tags ---- */
.tag-row { display: flex; flex-wrap: wrap; gap: var(--sp-2); }
.tag { font-size: var(--text-xs); background: var(--c-ice); color: var(--c-brand); padding: 3px var(--sp-3); border-radius: var(--r-full); font-weight: 600; }
.tag--outline { background: transparent; border: 1px solid var(--c-border-md); color: var(--c-text-lt); }
.tag--outline.is-active { background: var(--c-ice); border-color: transparent; color: var(--c-brand); }

/* ---- Progress bar ---- */
.progress { height: 8px; border-radius: var(--r-full); background: var(--c-ice); overflow: hidden; }
.progress > span { display: block; height: 100%; max-width: 100%; background: var(--c-accent); border-radius: var(--r-full); }
/* Width is always capped at 100% in the markup, even past the target —
   overflow:hidden above would silently clip anything wider, making
   "exceeded" and "just barely finished" look identical. This color swap
   is what actually distinguishes them; the real percentage still shows in
   the card's own text (PLANNING.md §6.3). */
.progress > span.is-exceeded { background: var(--c-good); }
/* Bar width in whole percents, set by the |fill_class filter (apps/core/templatetags/core_extras.py)
   — a width can't be an inline style under the Content Security Policy. */
.fill-0 { width: 0%; } .fill-1 { width: 1%; } .fill-2 { width: 2%; } .fill-3 { width: 3%; } .fill-4 { width: 4%; } .fill-5 { width: 5%; } .fill-6 { width: 6%; } .fill-7 { width: 7%; } .fill-8 { width: 8%; } .fill-9 { width: 9%; }
.fill-10 { width: 10%; } .fill-11 { width: 11%; } .fill-12 { width: 12%; } .fill-13 { width: 13%; } .fill-14 { width: 14%; } .fill-15 { width: 15%; } .fill-16 { width: 16%; } .fill-17 { width: 17%; } .fill-18 { width: 18%; } .fill-19 { width: 19%; }
.fill-20 { width: 20%; } .fill-21 { width: 21%; } .fill-22 { width: 22%; } .fill-23 { width: 23%; } .fill-24 { width: 24%; } .fill-25 { width: 25%; } .fill-26 { width: 26%; } .fill-27 { width: 27%; } .fill-28 { width: 28%; } .fill-29 { width: 29%; }
.fill-30 { width: 30%; } .fill-31 { width: 31%; } .fill-32 { width: 32%; } .fill-33 { width: 33%; } .fill-34 { width: 34%; } .fill-35 { width: 35%; } .fill-36 { width: 36%; } .fill-37 { width: 37%; } .fill-38 { width: 38%; } .fill-39 { width: 39%; }
.fill-40 { width: 40%; } .fill-41 { width: 41%; } .fill-42 { width: 42%; } .fill-43 { width: 43%; } .fill-44 { width: 44%; } .fill-45 { width: 45%; } .fill-46 { width: 46%; } .fill-47 { width: 47%; } .fill-48 { width: 48%; } .fill-49 { width: 49%; }
.fill-50 { width: 50%; } .fill-51 { width: 51%; } .fill-52 { width: 52%; } .fill-53 { width: 53%; } .fill-54 { width: 54%; } .fill-55 { width: 55%; } .fill-56 { width: 56%; } .fill-57 { width: 57%; } .fill-58 { width: 58%; } .fill-59 { width: 59%; }
.fill-60 { width: 60%; } .fill-61 { width: 61%; } .fill-62 { width: 62%; } .fill-63 { width: 63%; } .fill-64 { width: 64%; } .fill-65 { width: 65%; } .fill-66 { width: 66%; } .fill-67 { width: 67%; } .fill-68 { width: 68%; } .fill-69 { width: 69%; }
.fill-70 { width: 70%; } .fill-71 { width: 71%; } .fill-72 { width: 72%; } .fill-73 { width: 73%; } .fill-74 { width: 74%; } .fill-75 { width: 75%; } .fill-76 { width: 76%; } .fill-77 { width: 77%; } .fill-78 { width: 78%; } .fill-79 { width: 79%; }
.fill-80 { width: 80%; } .fill-81 { width: 81%; } .fill-82 { width: 82%; } .fill-83 { width: 83%; } .fill-84 { width: 84%; } .fill-85 { width: 85%; } .fill-86 { width: 86%; } .fill-87 { width: 87%; } .fill-88 { width: 88%; } .fill-89 { width: 89%; }
.fill-90 { width: 90%; } .fill-91 { width: 91%; } .fill-92 { width: 92%; } .fill-93 { width: 93%; } .fill-94 { width: 94%; } .fill-95 { width: 95%; } .fill-96 { width: 96%; } .fill-97 { width: 97%; } .fill-98 { width: 98%; } .fill-99 { width: 99%; }
.fill-100 { width: 100%; }

/* ---- Tabs (in-page) ---- */
.tabs { display: flex; gap: var(--sp-2); border-bottom: 1px solid var(--c-border); margin-bottom: var(--sp-5); }
.tabs button, .tabs a { padding: var(--sp-3) var(--sp-4); font-family: var(--font-display); font-weight: 600; font-size: var(--text-sm); color: var(--c-text-lt); border-bottom: 2px solid transparent; margin-bottom: -1px; text-decoration: none; }
.tabs button.is-active, .tabs a.is-active { color: var(--c-dark); border-color: var(--c-accent); }
/* Search's numbered pager. Square 40px targets (the 44px a thumb wants, less
   a little so ‹ 1 … 4 5 6 … 36 › still fits a 390px phone on one line). */
.pager { display: flex; flex-wrap: wrap; justify-content: center; gap: calc(var(--sp-1) / 2); margin: var(--sp-4) 0; }
.pager a, .pager span { display: inline-flex; align-items: center; justify-content: center; min-width: 40px; height: 40px; padding: 0 var(--sp-2); border-radius: var(--r-md); font-family: var(--font-display); font-weight: 600; font-size: var(--text-sm); text-decoration: none; }
.pager a { color: var(--c-dark); border: 1px solid var(--c-border-md); background: #fff; }
.pager a:hover { border-color: var(--c-accent); }
.pager .pager-current { background: var(--c-dark); color: #fff; }
.pager .pager-gap { min-width: 20px; padding: 0; }
.pager .pager-gap, .pager .pager-off { color: var(--c-text-lt); }
.pager .pager-off { opacity: 0.4; }
/* Browse-by-objective strip (library/_browse_objectives.html) */
.browse-strip { margin-top: var(--sp-5); }
.browse-tiles { display: grid; grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); gap: var(--sp-3); }
.browse-tiles--compact { grid-template-columns: repeat(auto-fill, minmax(120px, 1fr)); gap: var(--sp-2); }
.browse-tile { display: flex; flex-direction: column; gap: calc(var(--sp-1) / 2); padding: var(--sp-3); border: 1px solid var(--c-border-md); border-radius: var(--r-md); background: #fff; text-decoration: none; }
.browse-tile:hover { border-color: var(--c-accent); }
.browse-tile-name { font-family: var(--font-display); font-weight: 600; font-size: var(--text-sm); color: var(--c-dark); }
.browse-tile--all { background: var(--c-ice); }
.tab-panel { display: none; }
.tab-panel.is-active { display: block; }

/* ---- Tables ---- */
table { width: 100%; border-collapse: collapse; font-size: var(--text-sm); }
th { text-align: left; font-family: var(--font-display); font-size: var(--text-xs); text-transform: uppercase; letter-spacing: 0.03em; color: var(--c-text-lt); padding: var(--sp-2) var(--sp-3); border-bottom: 1px solid var(--c-border); }
td { padding: var(--sp-3); border-bottom: 1px solid var(--c-border); color: var(--c-text-md); }
/* One action per fixed column (e.g. Metrics' Tracked tables — Log / Goal /
   Stop / Archive) so the same action lines up in the same place on every
   row, instead of a variable-count button group that shifts per row. */
th.ta-center, td.ta-center { text-align: center; }

/* ---- Info disclosure ---- */
/* A small "ⓘ" that reveals extra explanatory text in place, for real
   (non-prototype) help copy that's worth keeping but not worth showing
   by default on every visit — e.g. a page's fuller explanation of how
   something works, once a short one-line summary already covers the
   essential fact. Reuses <details>/<summary> rather than a hover
   tooltip or JS popover: no positioning/collision logic to get wrong,
   works identically by tap, click, or keyboard, and matches the
   "optional depth, collapsed by default" pattern this app already uses
   for Vision's "Want to go deeper?" and Metrics' "View the logged sets
   this was calculated from." */
.info-disclosure { display: inline-block; vertical-align: text-bottom; margin-left: var(--sp-1); }
.info-disclosure summary { list-style: none; cursor: pointer; display: inline-flex; align-items: center; justify-content: center; width: 18px; height: 18px; border-radius: 50%; border: 1px solid var(--c-border-md); color: var(--c-text-lt); font-family: Georgia, 'Times New Roman', serif; font-size: 12px; font-weight: 700; font-style: italic; line-height: 1; }
.info-disclosure summary::-webkit-details-marker { display: none; }
.info-disclosure summary:hover, .info-disclosure summary:focus-visible { color: var(--c-brand); border-color: var(--c-brand); }
.info-disclosure[open] summary { color: var(--c-brand); border-color: var(--c-brand); }
.info-disclosure-body { display: block; margin-top: var(--sp-2); padding: var(--sp-3); max-width: 420px; background: var(--c-ice-2); border-radius: var(--r-md); font-size: var(--text-sm); color: var(--c-text-md); }

/* A <section-title>-weight heading that's also a <details> toggle — Vision's
   "A few questions, if they help" is a one-time prompt for picking an
   image, not something a returning user (who already has a board) needs
   sitting open above the fold every visit; collapsed by default once
   they've added at least one image, same "optional depth" pattern as
   .info-disclosure above. */
.section-toggle { cursor: pointer; list-style: none; display: flex; align-items: center; gap: var(--sp-2); }
.section-toggle::-webkit-details-marker { display: none; }
.section-toggle h3 { margin: 0; }
.section-toggle::after { content: "▸"; color: var(--c-text-lt); font-size: var(--text-xs); }
details[open] > .section-toggle::after { content: "▾"; }

/* A visible, permanent note about prototype-only behavior, inline in a
   page's own content (not the top-of-page banner) — styled distinctly
   from real product copy so it reads unambiguously as "this won't be
   here once it's real," not as another piece of help text. */
.proto-note { display: flex; gap: var(--sp-2); align-items: flex-start; font-size: var(--text-xs); font-style: italic; color: var(--c-text-lt); border-top: 1px dashed var(--c-border-md); padding-top: var(--sp-3); }
tr:last-child td { border-bottom: none; }

/* ---- List rows (search results, exercise rows) ---- */
.item-row { display: flex; align-items: center; gap: var(--sp-4); padding: var(--sp-4); border: 1px solid var(--c-border); border-radius: var(--r-md); background: var(--c-surface); margin-bottom: var(--sp-3); transition: var(--t-fast); }
a.item-row:hover, button.item-row:hover { border-color: var(--c-brand); box-shadow: var(--shadow-sm); }
/* A whole result row as the clickable target (Freestyle Session's exercise
   search), not just a button off to one side — "hit Select on the far
   right" isn't an obvious tap target on a result you'd expect to just
   tap directly. */
button.item-row { width: 100%; text-align: left; cursor: pointer; font: inherit; }
.item-row .info { flex: 1; min-width: 0; }
/* A short number field sitting in a row beside its button (a suggested goal weight). */
.item-row input.input-sm { width: 110px; flex: none; }
/* Workout/exercise rows (Search, Train) can grow downward — View sessions,
   a description's Show more — so their actions stay pinned beside the name
   rather than drifting to the middle of the expanded row, where they'd read
   as belonging to whatever is open (one session, one exercise). */
.item-row--top { align-items: flex-start; }
.item-row .info h4 { margin-bottom: 2px; }
.item-row .meta { font-size: var(--text-xs); color: var(--c-text-lt); }
/* A row's description: clamped to two lines; initClampedText adds a Show
   more toggle only when it actually overflows. */
.item-desc { font-size: var(--text-sm); color: var(--c-text-md); margin-bottom: 0; }
.clamp-2 { display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; }
.clamp-2.is-expanded { display: block; }
.instructions-body { white-space: pre-line; margin-top: var(--sp-2); }
button.details-toggle { background: none; border: none; padding: var(--sp-3) 0; font-family: inherit; }

/* ---- Vision board ---- */
.vision-grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: var(--sp-4); }
/* Flex column, not a plain block — a photo tile's caption is now a real
   row below the image (see .caption below), not an overlay on top of it,
   so it needs somewhere of its own to live regardless of how the image
   contain-fits above it. */
.vision-tile { position: relative; aspect-ratio: 1; border-radius: var(--r-lg); overflow: hidden; background: var(--c-bg); border: 1px solid var(--c-border); display: flex; flex-direction: column; }
/* contain, not cover — a cropped-to-fill square was cutting real content
   off the top and bottom of every portrait phone photo (most of them).
   Background matches the page (--c-bg, not --c-surface) so the letterbox
   fill on whichever sides don't reach the edge reads as "no photo there"
   rather than as its own visible box. flex:1/min-height:0 let the image
   shrink to make room for the caption row below without either one
   fighting the tile's own fixed aspect-ratio square. */
.vision-tile img { width: 100%; flex: 1; min-height: 0; object-fit: contain; }
/* A real row below the image, not an absolutely-positioned overlay on
   top of it — the old bottom-strip-over-the-photo design assumed the
   photo always reached every edge (true under object-fit:cover, no
   longer true under contain), so a short/landscape photo left empty
   margin the caption would either float in or spill across the photo's
   own edge depending on caption length. This can't happen anymore: the
   caption has its own space, full stop. */
.vision-tile .caption { flex-shrink: 0; padding: var(--sp-2) var(--sp-3); background: var(--c-bg); color: var(--c-text); font-size: var(--text-xs); font-weight: 600; text-align: center; }
.vision-tile.is-add { display: flex; align-items: center; justify-content: center; flex-direction: column; gap: var(--sp-2); color: var(--c-text-lt); border: 1.5px dashed var(--c-border-md); background: transparent; cursor: pointer; }
.vision-tile-remove { position: absolute; top: var(--sp-2); right: var(--sp-2); z-index: 2; width: 28px; height: 28px; border-radius: 50%; border: none; background: rgba(27,58,75,0.6); color: #fff; font-size: var(--text-xs); line-height: 1; display: flex; align-items: center; justify-content: center; cursor: pointer; }
.vision-tile-remove:hover, .vision-tile-remove:focus-visible { background: var(--c-bad); }
/* The "Show on Home" toggle under a board tile. */
.tile-pin { display: flex; align-items: center; justify-content: center; gap: var(--sp-2); margin-top: var(--sp-2); }
/* Text-only tile ("light" WYSIWYG, PLANNING.md §6.5/§5 Iteration 3) —
   the caption becomes the whole tile, centered, not a bottom strip
   over a photo. Centering lives on the caption itself (absolute,
   inset:0), not on .vision-tile — the tile stays a plain
   position:relative block same as every other tile, rather than also
   becoming a flex container that the remove button (a sibling of the
   caption, also absolutely positioned) would otherwise have to
   compete with for layout/paint order. Fixed 2026-09-16 after the
   remove "✕" went missing on text tiles under the earlier flex-on-
   the-tile version. */
.vision-tile.is-text .caption { position: absolute; inset: 0; display: flex; align-items: center; justify-content: center; background: none; padding: var(--sp-4); font-size: var(--text-sm); font-weight: 700; line-height: 1.3; text-align: center; color: #fff; }

/* Home's top-3 Vision preview strip (core/dashboard.html) — same tile
   treatment as the board itself (object-fit:contain, page-matching
   background, real caption row/overlay), just at a smaller fixed size
   sized to sit next to the label/Edit button. Sized as large as
   comfortably fits on desktop; on mobile (<=640px) the label+Edit move to
   their own row above so the tiles get the full content width instead
   and grow to fill it, capped so they don't get oversized on a wide
   phone. */
/* Profile's Coaching tab — the toggle + "Workout & session data" label row
   on each Coach card. */
.coach-toggle-row { display: flex; align-items: center; gap: var(--sp-3); padding: var(--sp-2) 0; }
.dash-daily-status { display: flex; align-items: center; gap: var(--sp-2); }
.dash-daily-status-label { font-weight: 600; }
.dash-daily-status-picker { display: flex; gap: 4px; }
.dash-daily-status-done { display: flex; align-items: center; gap: var(--sp-2); }

/* Daily-status face icons — one sprite sheet (4 equal frames, normalized to
   the same circle size/center so none of them drift in scale or position),
   selected via background-position rather than 4 separate image requests. */
.face-icon {
  display: inline-block;
  width: 26px;
  height: 26px;
  flex-shrink: 0;
  background-image: url("../img/daily-status-faces.95a4874a172c.png");
  background-repeat: no-repeat;
  background-size: 400% 100%;
}
.face-icon--1 { background-position: 0% 0; }
.face-icon--2 { background-position: 33.333% 0; }
.face-icon--3 { background-position: 66.667% 0; }
.face-icon--4 { background-position: 100% 0; }
/* The four faces as a choice on the Log a Value page — a radio each, the
   selected face ringed in the accent colour. */
.face-choices { display: flex; gap: var(--sp-2); flex-wrap: wrap; }
.field .face-choice, .face-choice { position: relative; display: flex; flex-direction: column; align-items: center; gap: var(--sp-1); min-width: 62px; padding: var(--sp-2); border: 1px solid var(--c-border); border-radius: var(--r-md); font-size: var(--text-xs); text-align: center; cursor: pointer; margin: 0; }
.face-choice .face-icon { width: 40px; height: 40px; }
/* A face inside a line of text (a Metrics row's latest value) sits with it. */
.meta .face-icon { width: 18px; height: 18px; vertical-align: middle; }
.face-choice input { position: absolute; opacity: 0; pointer-events: none; }
.face-choice:has(input:checked) { border-color: var(--c-accent); box-shadow: 0 0 0 2px var(--c-accent); }
.face-choice:has(input:focus-visible) { outline: 2px solid var(--c-brand); outline-offset: 2px; }
.dash-user-grid { row-gap: var(--sp-6); }
.dash-vision-strip { display: flex; align-items: center; gap: var(--sp-4); margin-bottom: var(--sp-5); padding-bottom: var(--sp-4); border-bottom: 1px solid var(--c-border); }
/* Just an <h3> — same heading style as "Goals"/"Analysis" elsewhere on
   this page. flex-shrink:0 only, everything else comes from the h3 rule. */
.dash-vision-label { flex-shrink: 0; }
.dash-vision-tiles { display: flex; gap: var(--sp-3); flex: 1; min-width: 0; text-decoration: none; }
/* Slightly taller than square: a phone photo is portrait, and contained in a
   square it sat small in the middle. They may shrink to fit a narrow window. */
.dash-vision-tiles .vision-tile { flex: 0 1 150px; aspect-ratio: 4 / 5; }
.dash-vision-tiles .vision-tile.is-text .caption { padding: var(--sp-2); font-size: var(--text-xs); line-height: 1.15; }
.dash-vision-strip > .btn { flex-shrink: 0; }
/* Home's first-tile question: the label, the question, then one line to answer it. */
.dash-vision-ask { margin-bottom: var(--sp-5); padding-bottom: var(--sp-4); border-bottom: 1px solid var(--c-border); }
.dash-vision-ask .dash-vision-label { margin-bottom: var(--sp-2); }
.dash-vision-ask .vision-question { display: block; margin-bottom: var(--sp-2); }
.dash-vision-ask-row { display: flex; gap: var(--sp-2); align-items: center; }
.dash-vision-ask-row input { flex: 1; min-width: 0; }
.dash-vision-ask-row .btn { flex-shrink: 0; }
@media (max-width: 640px) { .dash-vision-ask-row { flex-wrap: wrap; } .dash-vision-ask-row .btn { width: 100%; } }
/* The member's own words, quoted above their workouts (Train → Active). */
.why-line { margin: 0 0 var(--sp-4); padding-left: var(--sp-3); border-left: 3px solid var(--c-accent); font-style: italic; color: var(--c-text-md); }
/* Home's "This week" strip: a dot for each day, filled when the member was active. */
.dash-week { display: flex; align-items: center; flex-wrap: wrap; gap: var(--sp-3); margin-bottom: var(--sp-5); padding-bottom: var(--sp-4); border-bottom: 1px solid var(--c-border); }
.dash-week-label { flex-shrink: 0; }
.week-dots { display: flex; gap: var(--sp-2); list-style: none; margin: 0; padding: 0; }
.week-dot { width: 32px; height: 32px; border-radius: 50%; border: 1.5px solid var(--c-border-md); display: flex; align-items: center; justify-content: center; font-family: var(--font-display); font-size: var(--text-xs); font-weight: 600; color: var(--c-text-lt); }
.week-dot.is-active { background: var(--c-good); border-color: var(--c-good); color: #fff; }
.week-dot.is-today { box-shadow: 0 0 0 2px var(--c-brand); }
.week-dot.is-future { opacity: 0.55; }
.dash-week-note { margin: 0; font-size: var(--text-sm); }
.dash-week > .btn { margin-left: auto; flex-shrink: 0; }
@media (max-width: 640px) { .dash-week > .btn { width: 100%; margin-left: 0; } }
.dash-vision-empty { display: flex; align-items: center; flex-wrap: wrap; gap: var(--sp-3); margin-bottom: var(--sp-5); padding-bottom: var(--sp-4); border-bottom: 1px solid var(--c-border); }
.dash-vision-empty > .btn { margin-left: auto; flex-shrink: 0; }
@media (max-width: 640px) {
  .dash-vision-strip { flex-wrap: wrap; row-gap: var(--sp-3); }
  /* Scoped to .dash-vision-strip specifically — .dash-vision-label is
     also used (unreordered) in .dash-vision-empty, which needs no
     reordering since it's just title/message/button in natural order. */
  .dash-vision-strip .dash-vision-label { order: 1; }
  .dash-vision-strip > .btn { order: 2; margin-left: auto; }
  .dash-vision-tiles { order: 3; flex-basis: 100%; gap: var(--sp-2); }
  .dash-vision-tiles .vision-tile { flex: 1; max-width: 150px; }
}

/* Vision Board's printable page (vision_print.html) — a standalone
   document, not app.css's usual app-shell/card look, so it gets its own
   small ruleset rather than reusing .vision-tile (built for an
   interactive card: hover states, an overlay remove button, px-sized
   tiles — none of which a printed page needs, and fighting them would
   cost more than this does). Text tiles print as a plain bordered box,
   not the gradient background .is-text uses on screen — deliberately:
   browsers only print background colors/images when the person has
   "print background graphics" on in their print dialog, which most
   don't, and a caption that depends on that to stay legible is a bad
   bet for something meant to end up on a fridge. Plain black-on-white
   prints correctly regardless of that setting. */
.print-page { max-width: 7.5in; margin: 0 auto; padding: var(--sp-5); }
.print-toolbar { display: flex; gap: var(--sp-3); margin-bottom: var(--sp-5); }
.print-header { text-align: center; margin-bottom: var(--sp-5); }
.print-header p { color: var(--c-text-lt); font-size: var(--text-sm); margin-top: var(--sp-1); }
/* The mark above a printed page's title: quiet, centred, black like the rest. */
.print-brand { display: flex; align-items: center; justify-content: center; gap: var(--sp-2); margin-bottom: var(--sp-2); font-family: var(--font-brand); font-weight: 700; font-size: var(--text-base); color: #000; }
.print-brand img { width: 1.5rem; height: 1.5rem; }
.vision-print-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 0.2in; }
.vision-print-tile { break-inside: avoid; }
.vision-print-tile img { width: 100%; aspect-ratio: 1; object-fit: cover; border-radius: var(--r-md); border: 1px solid var(--c-border-md); }
.vision-print-tile.is-text { aspect-ratio: 1; display: flex; align-items: center; justify-content: center; padding: var(--sp-3); border: 1px solid var(--c-border-md); border-radius: var(--r-md); text-align: center; }
.vision-print-tile.is-text .vision-print-caption { margin-top: 0; font-weight: 700; }
.vision-print-caption { margin-top: var(--sp-1); font-size: var(--text-xs); text-align: center; }
.vision-print-empty { text-align: center; color: var(--c-text-lt); }
@media print {
  @page { size: portrait; margin: 0.5in; }
  .print-toolbar { display: none; }
}

/* The exercise guide that goes with the sheet (library/workout_guide.html): one
   exercise at a time, its two pictures large enough to judge a movement by, and
   never split across a page. */
.guide-exercise { margin-bottom: var(--sp-6); break-inside: avoid; }
.guide-exercise h2 { margin-bottom: var(--sp-2); }
.guide-images { display: flex; gap: var(--sp-4); margin-bottom: var(--sp-3); }
.guide-images figure { margin: 0; text-align: center; }
.guide-images img { display: block; width: 2.6in; max-width: 100%; border: 1px solid var(--c-border-md); border-radius: var(--r-md); }
.guide-images figcaption { font-size: var(--text-xs); color: var(--c-text-lt); }
.guide-instructions { white-space: pre-line; font-size: var(--text-sm); }

/* The workout sheet (library/workout_print.html) — a write-in log to take to
   the gym: every session on its own page, one ruled blank cell per prescribed
   set. Same standalone, plain black-on-white approach as the Vision print
   page above (.print-page / .print-toolbar / .print-header are shared). */
.print-header h1 { margin-bottom: var(--sp-1); }
.print-session { margin-bottom: var(--sp-8); }
.print-date { margin-top: var(--sp-3); font-size: var(--text-sm); }
.print-table { width: 100%; table-layout: fixed; border-collapse: collapse; margin-top: var(--sp-4); }
.print-table th:first-child { width: 30%; }
.print-table th, .print-table td { border: 1px solid #000; padding: var(--sp-2) var(--sp-3); font-size: var(--text-sm); vertical-align: top; text-align: left; }
.print-table th { background: none; color: #000; }
.print-table .print-set { width: 0.5in; text-align: center; }
.print-table .print-exercise-note { font-size: var(--text-xs); font-style: italic; }
.print-table .print-actual { width: 36%; height: 0.42in; }
.print-table tr:last-child td { border-bottom: 1px solid #000; }
.print-table .print-group td { font-weight: 700; border-left: 0; border-right: 0; border-top: 3px solid #000; background: #e5e5e5; padding-top: var(--sp-3); }
/* A linked group reads as one block even without colour: a heavy bar down the left edge of its exercise cells (the cell spans all of an exercise's sets, so one bar, not one per set). */
.print-table .print-in-group .print-exercise { border-left: 5px solid #000; }
.print-footer { display: flex; gap: var(--sp-5); margin-top: var(--sp-4); font-size: var(--text-sm); }
.print-line { flex: 1; border-bottom: 1px solid #000; padding-bottom: var(--sp-1); }
.print-notes { margin-top: var(--sp-4); height: 1.1in; border: 1px solid #000; padding: var(--sp-2) var(--sp-3); font-size: var(--text-sm); }
@media print {
  .print-session { margin-bottom: 0; break-after: page; }
  .print-session:last-of-type { break-after: auto; }
  .print-table tr { break-inside: avoid; }
}

/* A card/section title that's also a real link — keeps the heading's own
   color/weight (unlike .link's brand-blue) so it doesn't look like a
   second, competing style of text; the underline-on-hover is the only
   signal it's clickable. */
/* A name that opens something (a goal, a metric, a workout on Home): brand colour and a thin
   underline, so it reads as a link beside the plain card titles that are not. */
.title-link { color: var(--c-brand); text-decoration: underline; text-decoration-thickness: 1px; text-underline-offset: 3px; text-decoration-color: color-mix(in srgb, var(--c-brand) 45%, transparent); }
.title-link:hover { text-decoration-color: currentColor; }
/* The Vision text-tile palette (VISION_PALETTE in apps/vision/models.py): the one place
   each name's gradient is defined. Tiles and the swatches below both use these. */
.palette-brand { background: linear-gradient(135deg, var(--c-brand-lt), var(--c-brand)); }
.palette-accent { background: linear-gradient(135deg, var(--c-accent-lt), var(--c-accent)); }
.palette-good { background: linear-gradient(135deg, var(--c-good-lt), var(--c-good)); }
.palette-bad { background: linear-gradient(135deg, var(--c-bad-lt), var(--c-bad)); }
/* Preset color swatches — the same gradients as the text tiles themselves. */
.vision-color-swatch { width: 32px; height: 32px; border-radius: 50%; border: 2px solid transparent; cursor: pointer; padding: 0; }
.vision-color-swatch.is-active { border-color: var(--c-brand); }
/* The three "Why it matters / Where you're headed / The next step"
   prompt cards' small uppercase eyebrow label and question heading. */
.eyebrow { font-size: var(--text-xs); font-weight: 700; text-transform: uppercase; letter-spacing: 0.03em; color: var(--c-brand); margin-bottom: var(--sp-2); }
.vision-question { font-size: var(--text-lg); font-weight: 700; font-family: var(--font-display); margin-bottom: var(--sp-3); }
.details-toggle { cursor: pointer; color: var(--c-brand); font-weight: 600; font-size: var(--text-sm); }
/* Show more / View sessions are text links, but a thumb needs a ~44px-tall target, not the 20px of the words. */
summary.details-toggle { padding: var(--sp-3) 0; }
/* The "it's been a while" progress-photo reminder — same dashed/tinted
   treatment as .empty-state--boxed, but a row layout, not centered. */
.nudge-card { display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap; gap: var(--sp-3); border-style: dashed; background: var(--c-ice-2); }

/* ---- Sparkline chart placeholder ---- */
.sparkline { width: 100%; height: 46px; }
.sparkline path.line { fill: none; stroke: var(--c-accent); stroke-width: 2; }
.sparkline path.area { fill: var(--c-accent-dim); stroke: none; }
/* One reading is a dot (a zero-length segment with a round cap), not a line. */
.sparkline path.line.is-dot { stroke-width: 7; stroke-linecap: round; vector-effect: non-scaling-stroke; }
/* A multi-line sparkline (a pinned group's card): the lines of the full chart,
   colour plus dash, and no area fill. */
.sparkline path.line--1 { stroke: var(--c-brand); }
.sparkline path.line--2 { stroke: var(--c-accent); stroke-dasharray: 5 3; }
.sparkline path.line--3 { stroke: var(--c-good); stroke-dasharray: 2 2; }
.sparkline path.line--4 { stroke: var(--c-dark); stroke-dasharray: 6 2 2 2; }
/* A Goal's target reference line — dashed, so it reads as "the line to
   beat," not a second data series. Line/area switch to --c-good once a
   goal is exceeded (progress > 100%), same signal as .progress's
   is-exceeded state below — PLANNING.md §6.3. */
.sparkline .target-line { stroke: var(--c-text-lt); stroke-width: 1; stroke-dasharray: 3 3; }
/* A group goal's chart: each line's target mark in that line's colour. */
.sparkline .target-line--1 { stroke: var(--c-brand); }
.sparkline .target-line--2 { stroke: var(--c-accent); }
.sparkline .target-line--3 { stroke: var(--c-good); }
.sparkline .target-line--4 { stroke: var(--c-dark); }
.sparkline path.line.is-exceeded { stroke: var(--c-good); }
.sparkline path.area.is-exceeded { fill: var(--c-good-dim); }
/* A trend chart with labelled axes. The axis labels are SVG <text> placed by
   percentage in their own SVGs (metrics/_trend_chart.html) so they keep their
   shape at any width; only the line stretches, with a constant stroke width. */
.chart { display: grid; grid-template-columns: 18px 40px 1fr; grid-template-rows: 220px 22px auto; column-gap: var(--sp-1); margin-top: var(--sp-3); }
.chart svg { overflow: visible; }
.chart-title-y { grid-row: 1; grid-column: 1; writing-mode: vertical-rl; transform: rotate(180deg); text-align: center; font-size: var(--text-xs); color: var(--c-text-lt); overflow: hidden; }
.chart-yaxis { grid-row: 1; grid-column: 2; width: 100%; height: 100%; }
.chart-plot { grid-row: 1; grid-column: 3; width: 100%; height: 100%; }
.chart-xaxis { grid-row: 2; grid-column: 3; width: 100%; height: 100%; }
.chart-title-x { grid-row: 3; grid-column: 3; text-align: center; font-size: var(--text-xs); color: var(--c-text-lt); }
.chart .grid-line { stroke: var(--c-border); stroke-width: 1; }
.chart path.line { fill: none; stroke: var(--c-brand); stroke-width: 2.5; vector-effect: non-scaling-stroke; }
.chart circle.pt { fill: var(--c-brand); }
/* Readings (initChartReadouts): shown as small dots on a short line, and only
   under the pointer on a long one. Touch slides along the chart sideways while
   the page still scrolls up and down. */
.chart { position: relative; }
.chart-plot { touch-action: pan-y; }
.chart circle.pt-reading { fill: var(--c-brand); opacity: 0; pointer-events: none; }
.chart--dots circle.pt-reading { opacity: .85; }
.chart circle.pt-reading.is-active { opacity: 1; stroke: var(--c-surface); stroke-width: 2; }
.chart circle.pt-reading--2 { fill: var(--c-accent); }
.chart circle.pt-reading--3 { fill: var(--c-good); }
.chart circle.pt-reading--4 { fill: var(--c-dark); }
.chart-tip { position: absolute; transform: translateX(-50%); z-index: 2; pointer-events: none; white-space: nowrap; padding: var(--sp-1) var(--sp-2); border-radius: var(--r-sm); background: var(--c-dark); color: var(--c-surface); font-size: var(--text-xs); box-shadow: var(--shadow-md); }
/* The lines of a multi-metric chart (metrics/_multi_trend_chart.html): colour
   plus a different dash each, so they read without relying on colour. */
.chart path.line--1 { stroke: var(--c-brand); }
.chart path.line--2 { stroke: var(--c-accent); stroke-dasharray: 7 4; }
.chart path.line--3 { stroke: var(--c-good); stroke-dasharray: 2 3; }
.chart path.line--4 { stroke: var(--c-dark); stroke-dasharray: 9 3 2 3; }
.chart-legend .swatch--1, .chart-legend .swatch--2, .chart-legend .swatch--3, .chart-legend .swatch--4 { width: 18px; height: 0; border-top: 3px solid; background: none; border-radius: 0; vertical-align: 3px; }
.chart-legend .swatch--1 { border-color: var(--c-brand); }
.chart-legend .swatch--2 { border-color: var(--c-accent); border-top-style: dashed; }
.chart-legend .swatch--3 { border-color: var(--c-good); border-top-style: dotted; }
.chart-legend .swatch--4 { border-color: var(--c-dark); border-top-style: dashed; }
.chart circle.pt--1 { fill: var(--c-brand); }
.chart circle.pt--2 { fill: var(--c-accent); }
.chart circle.pt--3 { fill: var(--c-good); }
.chart circle.pt--4 { fill: var(--c-dark); }
.axis-label { font-family: var(--font-display); font-size: 11px; fill: var(--c-text-lt); }
/* A sparkline with just its scale: top and bottom values beside the line, the
   first and last dates under it (metrics/_spark.html). */
.spark { display: grid; grid-template-columns: 34px 1fr; column-gap: var(--sp-1); }
.spark-yaxis { width: 100%; height: 46px; overflow: visible; }
.spark-xaxis { grid-column: 2; display: flex; justify-content: space-between; font-size: var(--text-xs); color: var(--c-text-lt); }
.chart-legend { display: flex; gap: var(--sp-4); margin-top: var(--sp-3); font-size: var(--text-xs); color: var(--c-text-lt); }
.chart-legend .swatch { display: inline-block; width: 10px; height: 10px; border-radius: 2px; margin-right: 4px; vertical-align: -1px; }
/* Always brand-colored — a view only ever charts one metric at a time. */
.chart-legend .swatch--brand { background: var(--c-brand); }
/* The metric-picker <select> in Analysis's card-head — sits next to the
   date-range tags, so it shouldn't stretch to the card's full width. */
.select-inline { width: auto; min-width: 240px; }

/* ---- Empty / misc ---- */
.row { display: flex; align-items: center; }
.row-between { display: flex; align-items: center; justify-content: space-between; }
/* A checkbox/tag group that should wrap onto its own lines once it runs
   out of room, rather than overflow or squeeze (Goals' "Share with"). */
.wrap-row { display: flex; gap: var(--sp-4); flex-wrap: wrap; }
.gap-2 { gap: var(--sp-2); }
.gap-3 { gap: var(--sp-3); }
.gap-4 { gap: var(--sp-4); }
.items-start { align-items: start; }
/* Visually hidden but still announced — a <label> a compact layout has no
   room to show, e.g. Search's bar (placeholder already states the field's
   purpose on screen). */
.sr-only { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip: rect(0,0,0,0); white-space: nowrap; border: 0; }
.fw-600 { font-weight: 600; }
.section-title { display: flex; align-items: center; justify-content: space-between; margin-bottom: var(--sp-4); }
.section-title h3 { margin: 0; }
.muted { color: var(--c-text-lt); font-size: var(--text-sm); }
/* Both come after .muted so combining `muted text-xs`/`muted text-sm`
   actually wins the tie — equal specificity, later source wins. */
.text-xs { font-size: var(--text-xs); }
.text-sm { font-size: var(--text-sm); }
/* A one-line muted caption under a heading — e.g. a goal's target date
   and who set it. Self-contained so it doesn't depend on the .muted
   cascade order above. */
.meta-line { display: block; font-size: var(--text-xs); color: var(--c-text-lt); }
/* A smaller, dimmer variant of .field's own label — "Share with" under
   a goal, not the field's main label. */
.field .label-dim { font-size: var(--text-xs); font-weight: 600; color: var(--c-text-md); margin-bottom: var(--sp-2); display: block; }
.notice-warn { font-size: var(--text-sm); font-weight: 600; color: var(--c-warn); }
.divider { height: 1px; background: var(--c-border); margin: var(--sp-5) 0; }

/* ---- Mobile session logger ---- */
.session-shell { max-width: 480px; margin: 0 auto; }
/* Extra clearance under the last card so the floating rest timer never
   overlaps Finish Session/Complete and End Workout — the two lightweight
   recording pages (base_session.html) are the only place .session-shell
   wraps .app-content instead of a bare header. */
.app-content.session-shell { padding-bottom: 96px; }

/* ---- Session Logging / Freestyle Session set tables ----
   Real markup here is a plain <table> (data-set-row on the <tr>), not the
   .set-row grid above (prototype-only, unused by either real page — see
   that section's own comment). table-layout:fixed with an explicit width
   per column, added after a real usability report: an auto-layout table
   let the (now-removed, see apps/core/forms.py::WeightField) native
   number-input spin buttons plus the 🧮/Done/Remove columns force the
   table wider than its card, pushing Remove out past the card's own
   border, and left Notes near-illegible-narrow — whatever was left over
   after every other column claimed its native default width first.
   .col-notes is the one column deliberately left unsized, so it always
   absorbs whatever width the fixed columns don't need; column count
   varies by exercise/page (only weight-and-reps has a 🧮 column, Freestyle Session has no
   Done column), which is why each table's own <colgroup> only includes
   the <col> classes it actually needs. */
table.set-table { table-layout: fixed; }
table.set-table th, table.set-table td { padding: 6px 3px; overflow: hidden; }
table.set-table col.col-idx { width: 1.8rem; }
table.set-table col.col-num { width: 3.6rem; }
table.set-table col.col-icon { width: 1.8rem; }
table.set-table col.col-remove { width: 1.6rem; }
table.set-table input[type="text"] { width: 100%; box-sizing: border-box; padding: 6px 2px; text-align: center; }
/* Free text reads oddly centered while you're typing it — left-align just
   this one column, same deviation from the prototype's own (centered)
   Notes column made here on purpose. */
table.set-table input[name$="-notes"] { text-align: left; }
/* Remove is icon-only here ("−", not "− Remove") — the visible text was
   what originally forced this column wide enough to help push the whole
   table past the card's right edge; `aria-label`/`title` on the button
   itself (log_session.html/freestyle_session.html) keep it a real, named action
   for assistive tech even though the visible glyph is just a dash. */
table.set-table [data-action="remove-set"],
table.set-table [data-action="remove-formset-row"] { width: 100%; padding: 6px 0; font-size: var(--text-sm); }
table.set-table .set-calc { display: block; width: 100%; }
/* Session-exercises builder: the set number is a counter (nothing to
   renumber on add/remove), and the first set never gets a Remove — it's the
   exercise's anchor, same as Freestyle Session; removing the exercise is its own
   button. */
[data-prescribed-sets] { counter-reset: prescribed-set; }
[data-prescribed-sets] .set-idx::before { counter-increment: prescribed-set; content: counter(prescribed-set); }
[data-prescribed-sets] tr:first-child [data-action="remove-formset-row"] { display: none; }
/* Trailing 32px column is Remove — same size as the idx column so the
   row reads balanced, added 2026-09-14 as the counterpart to + Add Set
   (PLANNING.md §6.4): a blank, unremovable row after adding one too many
   is confusing even though it's harmless (nothing blank gets recorded). */
.set-row { display: grid; grid-template-columns: 32px 1fr 1fr 1fr 40px 32px; gap: var(--sp-2); align-items: center; margin-bottom: var(--sp-2); }
/* Bodyweight-reps variant — no weight column, since there's nothing to
   enter (PLANNING.md's exercise-card design: every exercise is N sets,
   only the columns change per exercise). Same idea would extend to a
   bodyweight-*time* card (Set | Duration) if one's ever needed here. */
.set-row--bw { grid-template-columns: 32px 1fr 1fr 40px 32px; }
/* session-log.html's strength-only 🧮 column (added 2026-09-22, same
   usability feedback as set-row--quick.set-row--calc below) — one more
   column than the base 6-column .set-row, inserted before the existing
   done/remove pair rather than after, so the calculator sits next to
   the Weight field it fills, not off in the row's action-icon corner. */
.set-row--calc { grid-template-columns: 32px 1fr 1fr 1fr 32px 40px 32px; }
/* Not scoped under .set-row — Session Logging/Freestyle Session render sets as a
   plain <table> (data-set-row on the <tr>, not class="set-row" on a grid
   div), so a `.set-row .set-idx` ancestor-scoped selector here would
   never actually match a real <td class="set-idx">. Found while wiring
   up .set-calc below for Freestyle Session's 🧮 button — same dead-selector bug
   would have applied there too. */
.set-idx { font-family: var(--font-display); font-weight: 700; color: var(--c-text-lt); text-align: center; }
.set-row input { text-align: center; padding: var(--sp-2); }
.set-row .set-remove { color: var(--c-text-lt); text-align: center; }
.set-row .set-remove:hover { color: var(--c-bad); }
.set-row .set-done { width: 100%; height: 40px; border-radius: var(--r-md); border: 1.5px solid var(--c-border-md); display: flex; align-items: center; justify-content: center; color: var(--c-text-lt); }
.set-row .set-done.is-done { background: var(--c-good-dim); border-color: var(--c-good); color: var(--c-good); }
/* Freestyle Session variant — no done column, since every row is already a
   logged set (no prescription to check off against), and a remove
   button on every row, not just the last (no live mid-workout mis-tap
   risk to guard against here). PLANNING.md §6.4, 2026-09-17. */
.set-row--quick { grid-template-columns: 32px 1fr 1fr 1fr 32px; }
.set-row--quick.set-row--bw { grid-template-columns: 32px 1fr 1fr 32px; }
/* Strength-only variant with a 🧮 target-weight helper column (added
   2026-09-22, usability feedback — "calculate my target weight based on
   my 1RM... and just have the app plug in the weights"). One extra
   column vs. set-row--quick's base 5, only ever applied where there's a
   real weight field to fill (never bodyweight/cardio rows, which have
   nothing for a %1RM calculation to plug into). */
.set-row--quick.set-row--calc { grid-template-columns: 32px 1fr 1fr 1fr 32px 32px; }
/* Same dead-selector issue as .set-idx above — real Freestyle Session markup has
   no .set-row ancestor, so this is unscoped too. */
.set-calc { color: var(--c-text-lt); text-align: center; font-size: var(--text-sm); }
.set-calc:hover { color: var(--c-brand); }
/* Small muted column-label row above a set table — same look already
   repeated inline on session-log.html; given a real class here so
   Freestyle Session doesn't add a fourth inline copy of it. */
.set-row-head { font-size: var(--text-xs); color: var(--c-text-lt); font-family: var(--font-display); font-weight: 700; }
/* Base sizing for a round icon/number toggle button (RPE picker, Daily
   Status faces) — previously only ever set via repeated inline style;
   given a real base rule so new uses don't add another copy. */
.face-btn { width: 40px; height: 40px; padding: 0; display: flex; align-items: center; justify-content: center; }
.timer { text-align: center; font-family: var(--font-display); font-size: 2.5rem; font-weight: 800; color: var(--c-dark); margin: var(--sp-4) 0; }
/* Floating rest timer — was a static card at the top of session-log.html,
   which meant scrolling all the way back up to use it mid-session; fixed
   2026-09-14 to stay reachable from anywhere on the page. */
.rest-timer-float { position: fixed; bottom: var(--sp-4); left: 50%; transform: translateX(-50%); z-index: 50; background: var(--c-surface); border: 1px solid var(--c-border); border-radius: var(--r-full); box-shadow: var(--shadow-md); padding: var(--sp-2) var(--sp-3); display: flex; align-items: center; gap: 6px; max-width: calc(100vw - var(--sp-4)); }
.rest-timer-display { font-family: var(--font-display); font-weight: 800; font-size: var(--text-lg); color: var(--c-dark); min-width: 3.2ch; text-align: center; }
/* Narrower than the standard .btn--sm horizontal padding — five controls
   now share this pill (−15/+15 steppers added alongside Start/Pause/
   Reset), and the default 16px side padding was sized for one or two
   buttons at a time, not five on a 360-400px phone screen. Vertical
   padding is left at the same var(--sp-2) every other button on this
   page uses — narrower tap targets are an acceptable tradeoff here,
   shorter ones aren't, for the same reduced-touch-accuracy reasons
   already driving every other control on this page (PLANNING.md §6.4). */
.rest-timer-float .btn { padding: var(--sp-2) 10px; font-size: var(--text-xs); }

/* Client-private workout rating (workout-instance.html) — click-to-fill
   stars, same ★ character search.html already uses for public ratings,
   so the visual language carries over once this feeds a real public
   aggregate later (PLANNING.md §6.4). */
.star-btn { font-size: 1.4rem; line-height: 1; color: var(--c-border-md); padding: 0 1px; }
.star-btn.is-filled { color: var(--c-accent); }
.star-picker { display: flex; gap: 2px; }
span.star-btn { cursor: default; }
/* A rating beside its caption — "Your rating ★★★★★" on a workout row, or the
   tappable stars on a finished one. */
.rate-row { display: flex; align-items: center; gap: var(--sp-2); margin: var(--sp-2) 0 0; }
form.rate-row .star-btn { background: none; border: 0; cursor: pointer; }

/* Exercise cards in a live session — full visibility of the whole
   session is deliberate (PLANNING.md §6.4): nothing is hidden behind
   "current only." No highlighted-border "current" state either
   (removed 2026-09-14, same review pass that dropped "Set X of Y") —
   every card gets equal visual weight; only the logged data itself
   (checkmarks, filled inputs) signals progress. */
.exercise-card summary { cursor: pointer; list-style: none; }
.exercise-card summary::-webkit-details-marker { display: none; }
/* A deliberately-linked sequence (superset, pre-fatigue, contrast set,
   jumbo set — all the same primitive, PLANNING.md §6.4) — bracketed
   together with no gap between the cards inside, distinct from the
   normal gap between freely-orderable exercises elsewhere in the
   session. */
.exercise-group { border-left: 3px solid var(--c-accent); padding-left: var(--sp-4); margin-bottom: var(--sp-4); }
.exercise-group .exercise-card { margin-bottom: var(--sp-3); }
.exercise-group .exercise-card:last-child { margin-bottom: 0; }
.group-label { font-family: var(--font-display); font-size: var(--text-xs); font-weight: 700; color: var(--group-color, var(--c-accent)); margin-bottom: var(--sp-2); }

/* ---- Techniques (strip sets, to failure, superset, contrast set, ...) ---- */
/* Builder: a technique menu per set row and one per exercise card ("apply to
   all sets"), both a native <details> popover of checkboxes. */
.card-actions { display: flex; align-items: center; gap: var(--sp-2); flex-wrap: wrap; justify-content: flex-end; }
.tech-menu { position: relative; display: inline-block; }
.tech-menu > summary { cursor: pointer; list-style: none; }
.tech-menu > summary::-webkit-details-marker { display: none; }
.tech-menu-body { position: absolute; z-index: 20; right: 0; top: 100%; margin-top: 4px; width: max-content; max-width: 300px; padding: var(--sp-3); display: flex; flex-direction: column; gap: var(--sp-3); background: var(--c-surface); border: 1px solid var(--c-border-md); border-radius: var(--r-md); box-shadow: var(--shadow-md); text-align: left; }
.tech-menu-body .check-row small { font-weight: 400; }
table.set-table col.col-tech { width: 8.5rem; }
/* The builder can carry Reps, Weight, 🧮, % 1RM and Technique at once; on a phone the columns tighten so the set's remove button stays on the card. */
@media (max-width: 480px) {
  table.set-table--prescribe col.col-idx { width: 2rem; }
  table.set-table--prescribe col.col-num { width: 3rem; }
  table.set-table--prescribe col.col-tech { width: 5rem; }
  table.set-table--prescribe col.col-remove { width: 1.4rem; }
}
/* The popover must escape the cell — every other set-table cell clips. */
table.set-table td.tech-cell { overflow: visible; }
.tech-cell .tech-menu { display: block; }
.tech-cell .tech-menu-body { left: 0; right: auto; }
.tech-cell .tech-menu > summary { padding: 5px 6px; border: 1px solid var(--c-border-md); border-radius: var(--r-sm); background: var(--c-surface); color: var(--c-text-lt); font-size: var(--text-xs); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.tech-cell .tech-menu > summary.has-techniques { color: var(--c-brand); border-color: var(--c-brand); font-weight: 600; }
/* A linked group of exercise cards (builder, Log Session): one accent bar
   down the left edge, the cards sitting close, and the group's type and note
   above the first. */
/* Linked groups side by side must not blur together: every other group takes a
   second colour (.group-alt), a group's cards share a faint tint of it, and a
   wider gap follows the group. */
.is-grouped { --group-color: var(--c-accent); --group-tint: var(--c-accent-dim); }
.is-grouped.group-alt { --group-color: var(--c-brand); --group-tint: rgba(44, 110, 138, 0.1); }
.card.is-grouped { border-left: 5px solid var(--group-color); background: linear-gradient(var(--group-tint), var(--group-tint)), var(--c-surface); }
.card.is-grouped:not(.group-end) { margin-bottom: var(--sp-2); }
.card.is-grouped.group-end { margin-bottom: var(--sp-8); }
.group-controls, .group-heading { display: flex; flex-wrap: wrap; align-items: center; gap: var(--sp-3); margin-bottom: var(--sp-4); }
.group-controls label, .group-heading .group-label { margin: 0; font-family: var(--font-display); font-size: var(--text-xs); font-weight: 700; }
.group-controls select, .group-controls input { width: auto; }
.group-controls input { flex: 1; min-width: 12rem; }
.group-heading .group-note { font-size: var(--text-sm); color: var(--c-text-md); }
/* The author's cue for one exercise, shown wherever the member sees that exercise. */
.exercise-note { font-size: var(--text-sm); color: var(--c-text-md); margin: var(--sp-2) 0 0; }
/* The same bracket on a read-only prescription table (Find/My Content's View
   sessions, the workout detail page): an accent bar down the group's rows. */
tr.group-row > td, tr.is-grouped > td:first-child { border-left: 5px solid var(--group-color); }
tr.is-grouped > td { background: var(--group-tint); }
tr.group-row > td { padding-bottom: 0; }
.tech-cell .tech-menu > summary { font-family: var(--font-display); }
/* Read-only: a technique shown as a small pill beside what it applies to. */
.tech-chip { display: inline-block; padding: 1px 8px; border-radius: var(--r-full); background: var(--c-accent-dim); color: var(--c-accent); font-family: var(--font-display); font-size: var(--text-xs); font-weight: 700; white-space: nowrap; }
.tech-chips { display: inline-flex; flex-wrap: wrap; gap: var(--sp-1); margin-left: var(--sp-2); vertical-align: middle; }

/* ---- Modal (native <dialog>) ---- */
dialog.modal { margin: auto; border: none; border-radius: var(--r-lg); padding: 0; max-width: 560px; width: 92vw; box-shadow: var(--shadow-lg); color: var(--c-text); }
dialog.modal::backdrop { background: rgba(27,58,75,0.55); }
.modal-head { display: flex; align-items: center; justify-content: space-between; gap: var(--sp-4); padding: var(--sp-5) var(--sp-6); border-bottom: 1px solid var(--c-border); }
.modal-head h3 { margin: 0; }
.modal-body { padding: var(--sp-6); max-height: 55vh; overflow-y: auto; }
.modal-body p { font-size: var(--text-sm); }
.modal-body h4 { margin: var(--sp-5) 0 var(--sp-2); }
.modal-body h4:first-child { margin-top: 0; }
.modal-body blockquote { border-left: 3px solid var(--c-border-md); padding-left: var(--sp-4); margin-bottom: var(--sp-3); font-style: italic; color: var(--c-text-md); font-size: var(--text-sm); }
.modal-foot { padding: var(--sp-4) var(--sp-6); border-top: 1px solid var(--c-border); display: flex; justify-content: flex-end; }
.modal-foot--split { justify-content: space-between; }
/* The "Thinking about: <question>" reminder inside the Add Image modal. */
.modal-hint { font-size: var(--text-sm); padding: var(--sp-3); background: var(--c-ice-2); border-radius: var(--r-md); }
.modal-flag { display: block; background: var(--c-warn-dim); color: var(--c-warn); font-size: var(--text-xs); font-weight: 700; padding: var(--sp-3); border-radius: var(--r-md); margin-bottom: var(--sp-4); }
.legal-h { font-family: var(--font-display); font-weight: 700; color: var(--c-dark); font-size: var(--text-sm); margin-bottom: var(--sp-1); }

/* ---- Filter groups (search.html's Filters modal, and the tag picker on workout/exercise creation) ---- */
.filter-section-label { font-family: var(--font-display); font-weight: 700; font-size: var(--text-xs); text-transform: uppercase; letter-spacing: 0.03em; color: var(--c-text-lt); margin: 0 0 var(--sp-2); }
.filter-group { border: none; padding: 0; margin: 0 0 var(--sp-5); }
.filter-group legend { font-family: var(--font-display); font-weight: 700; font-size: var(--text-sm); padding: 0; margin-bottom: var(--sp-2); }
.filter-options { display: flex; flex-wrap: wrap; gap: var(--sp-2) var(--sp-4); }
.filter-option { display: flex; align-items: center; gap: 6px; font-size: var(--text-sm); }
/* A category with tags filed under others (Bodypart) — one tag per line,
   indented by depth; branches collapse via the toggle initTagTrees adds. */
.filter-row { display: flex; align-items: center; gap: var(--sp-1); }
.filter-options--tree { flex-direction: column; flex-wrap: nowrap; gap: var(--sp-2); }
.tag-depth-1 { padding-left: var(--sp-5); }
.tag-depth-2 { padding-left: calc(var(--sp-5) * 2); }
.tag-depth-3 { padding-left: calc(var(--sp-5) * 3); }
.tag-depth-4 { padding-left: calc(var(--sp-5) * 4); }
.tag-toggle { flex: none; width: 1.25rem; text-align: center; background: none; border: none; padding: 0; font-size: var(--text-sm); color: var(--c-text-lt); line-height: 1; }
button.tag-toggle { cursor: pointer; }
/* Search's own bar — name input plus Filters/Search on one line, so the
   whole tag taxonomy (moved into the Filters modal above) no longer sits
   inline and pushes results below the fold before anything's even typed. */
.search-bar { display: flex; gap: var(--sp-3); }
.search-bar input[type="search"] { flex: 1; }
.filter-btn { display: inline-flex; align-items: center; gap: var(--sp-2); white-space: nowrap; }
.filter-btn .tag { background: var(--c-brand); color: #fff; }

/* ---- Removable tag/category chip (admin.html's Tag categories list) ---- */
.tag-remove { background: none; border: none; padding: 0; margin-left: 4px; font-size: 0.9em; color: inherit; opacity: 0.6; cursor: pointer; line-height: 1; }
.tag-remove:hover { opacity: 1; color: var(--c-bad); }

/* ---- Calculators (calculations.html, and the same helper embedded in
   Freestyle Session's per-set 🧮 modal) — added 2026-09-22, usability feedback.
   Every calculator card is inputs-left/result-right on desktop, stacked
   on mobile (same breakpoint .field-row already collapses at); given
   real classes rather than the inline font-display/2xl/800 styling
   metrics.html's "From your exercises" cards used, since that's now
   this app's no-new-inline-styles convention. */
/* BMR / TDEE: what the two terms mean, above the calculator. */
.calc-definitions { margin: 0 0 var(--sp-5); }
.calc-definitions dt { font-weight: 600; margin-top: var(--sp-3); }
.calc-definitions dd { margin: var(--sp-1) 0 0; color: var(--c-text-lt); }
.calc-card { display: grid; grid-template-columns: 1fr 1fr; gap: var(--sp-6); align-items: start; }
.calc-result { text-align: center; background: var(--c-ice-2); border-radius: var(--r-md); padding: var(--sp-5); }
.calc-result-value { font-family: var(--font-display); font-size: var(--text-2xl); font-weight: 800; color: var(--c-dark); }
.calc-result-label { font-size: var(--text-xs); color: var(--c-text-lt); margin-top: var(--sp-1); }
.calc-result-sub { font-size: var(--text-sm); color: var(--c-text-md); margin-top: var(--sp-2); }
@media (max-width: 640px) {
  .calc-card { grid-template-columns: 1fr; }
}

/* ---- Plan comparison (onboarding) ---- */
.plan-compare { width: 100%; border-collapse: collapse; font-size: var(--text-sm); margin-bottom: var(--sp-5); }
.plan-compare th, .plan-compare td { padding: var(--sp-2) var(--sp-3); border-bottom: 1px solid var(--c-border); text-align: left; }
.plan-compare th:not(:first-child), .plan-compare td:not(:first-child) { text-align: center; }
.plan-compare th { font-family: var(--font-display); font-size: var(--text-xs); color: var(--c-text-lt); }

/* ---- Responsive ---- */
@media (max-width: 960px) {
  .grid--3 { grid-template-columns: 1fr 1fr; }
  .vision-grid { grid-template-columns: repeat(2, 1fr); }
}
@media (max-width: 860px) {
  .app-sidebar { position: fixed; left: 0; top: 0; z-index: 20; transform: translateX(-100%); transition: transform var(--t-base); box-shadow: var(--shadow-lg); }
  .app-sidebar.is-open { transform: translateX(0); }
  .nav-burger { display: flex; align-items: center; justify-content: center; width: 36px; height: 36px; border-radius: var(--r-md); border: 1px solid var(--c-border-md); }
  .nav-burger span, .nav-burger span::before, .nav-burger span::after { content: ""; display: block; width: 16px; height: 2px; background: var(--c-dark); position: relative; }
  .nav-burger span::before { position: absolute; top: -5px; }
  .nav-burger span::after { position: absolute; top: 5px; }
  .field-row, .grid--2 { grid-template-columns: 1fr; }
}
@media (max-width: 640px) {
  .grid--3 { grid-template-columns: 1fr 1fr; }
  .vision-grid { grid-template-columns: repeat(2, 1fr); }
  .app-content { padding: var(--sp-4); }
  /* A tab row wider than the phone (Calculations' four, Train's four) used
     to push the whole page sideways. Wrapping onto a second line keeps every
     tab visible — a scrolling row hid the last one with nothing to say so. */
  .tabs { flex-wrap: wrap; }
  .tabs button, .tabs a { white-space: nowrap; padding-left: var(--sp-3); padding-right: var(--sp-3); }
  /* Dashboard topbar packed title + Daily Status widget (label + 4 face
     buttons) + role-switcher + Freestyle Session + avatar into one non-wrapping
     64px row — badly cramped at the 360-400px width this app targets
     (caught in the 2026-09-16 heuristic usability pass). Wrapping the
     Daily Status widget to its own full-width row below is the
     lowest-risk fix: no markup change, and it's the one topbar element
     unique to dashboard.html, so nothing else needs to move. */
  .app-topbar { flex-wrap: wrap; height: auto; min-height: var(--topbar-h); padding: var(--sp-3) var(--sp-4); row-gap: var(--sp-3); }
  #daily-status-widget { order: 3; flex-basis: 100%; }
  /* Search's bar — input, Filters, and Search all on one line squeeze the
     input past readability at this app's 360-400px floor; wrap the input
     to its own row above the two action buttons instead. */
  .search-bar { flex-wrap: wrap; }
  .search-bar input[type="search"] { flex-basis: 100%; }
  /* An .item-row with a longer .meta line and 2+ action buttons (Train's
     Active tab: progress + next-session text next to Record Session and
     Cancel Workout) squeezes .info down to a sliver at this app's
     360-400px floor, wrapping every couple words. Same fix as the search
     bar above: .info takes its own full-width row, the actions wrap to
     a second row below it instead of fighting it for space on one line. */
  .item-row { flex-wrap: wrap; }
  .item-row .info { flex-basis: 100%; }
}

/* Under a logged-set table: "Also record:" + one button per column the exercise doesn't record. */
.field-toggles { display: flex; flex-wrap: wrap; align-items: center; gap: var(--sp-2); }
.field-toggles [data-toggle-message] { flex-basis: 100%; margin: 0; }
/* A button that can't act right now but stays put (aria-disabled, so it can still say why when pressed). */
.btn.is-disabled { opacity: 0.45; cursor: not-allowed; }

/* An exercise's start/end form images (library/_exercise_images.html). */
.exercise-images { display: flex; gap: var(--sp-3); margin: var(--sp-2) 0 var(--sp-3); }
.exercise-images figure { margin: 0; text-align: center; }
.exercise-images button { display: block; padding: 0; border: 0; background: none; cursor: zoom-in; }
.exercise-images img { display: block; width: 72px; height: 72px; object-fit: cover; border: 1px solid var(--c-border); border-radius: var(--r-md); }
.exercise-images figcaption { margin-top: var(--sp-1); font-size: var(--text-xs); color: var(--c-text-lt); }

/* The enlarged start/end pair (app.js [data-enlarge-image]): both at the size
   the screen allows, side by side when they fit, stacked on a phone. */
dialog.modal--images { max-width: 1200px; width: 96vw; }
.modal--images .modal-body { max-height: 80vh; }
.image-pair { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(100%, 340px), 1fr)); gap: var(--sp-5); }
.image-pair figure { margin: 0; text-align: center; }
.image-pair img { display: block; width: 100%; max-height: 70vh; object-fit: contain; border-radius: var(--r-md); background: var(--c-ice); }
.image-pair figcaption { margin-top: var(--sp-2); font-size: var(--text-sm); color: var(--c-text-lt); }

/* The How-to dialog's embedded video (library/_howto_modal.html). */
.howto-frame { display: block; width: 100%; aspect-ratio: 16 / 9; border: 0; border-radius: var(--r-md); background: var(--c-ice); }

/* A ready-made goal has filled the New Goal form: say so, and flash the card so the eye lands there. */
.goal-picked { margin-bottom: var(--sp-4); padding: var(--sp-3); border-left: 3px solid var(--c-brand); background: var(--c-ice); font-size: var(--text-sm); }
.goal-picked[hidden] { display: none; }
@keyframes card-flash { from { box-shadow: 0 0 0 3px var(--c-brand); } to { box-shadow: 0 0 0 3px transparent; } }
.is-flash { animation: card-flash 1.6s ease-out; }

/* ---- Strength panel (Calculations Strength tab and the abacus) ---- */
.sc-estimate { display: grid; grid-template-columns: 1fr 1fr; gap: var(--sp-3); }
.calc-pick { width: 100%; text-align: left; padding: var(--sp-3) var(--sp-1); font-weight: 600; color: var(--c-brand); }
tr.is-suggested td { background: var(--c-ice-2); }
