:root {
  /* Cream, 20 Aug 2026. BenchBear was dark from the first build until Del picked a light
     ground off a contact sheet of fourteen. Warm sand rather than white: the identity
     colours are the same four ideas as before, darkened enough to letter with on paper.
     --muted is still the dimmer of the two greys, which on a light ground means the
     LIGHTER one — don't "fix" that back. */
  /* ── THE PROOF THIS FILE ARRIVED (28 Aug 2026) ─────────────────────────────────────────
     index.html reads this one property back on load. If it is empty the stylesheet did not
     apply — a 404 during a GitHub Pages tree swap, most likely — and the shell re-requests
     the file rather than leaving Del looking at a column of unstyled text. Do not remove it,
     and do not move it out of :root. */
  --dlog-css: 1;
  --bg: #f4efe6; --bg2: #fdfaf4; --bg3: #fdfaf4;
  --border: #e2d9c9; --border2: #d2c6b1;
  --text: #231f1a; --muted: #8d8272; --muted2: #6a6053;
  --accent: #b45527; --green: #4f7a3f; --blue: #3a6b96; --amber: #9a6a17; --red: #a63f30;
  /* Identity-only colours, added 17 Aug 2026 for the session/programme tiles. Deliberately NOT
     reused for state anywhere — see the tile block below for why the split matters. */
  --violet: #71519b; --teal: #2c7a6b;
  /* The workout tile's own fill, 29 Sept 2026: cut B off the tint sheet, plus a line (see
     .exercise-block). It was --bg3, two steps off the ground, and the tile edge barely read. Its own
     token so the 17 other --bg3 users stay as they are. */
  --tile: #ffffff;
}
/* ─── ONE COLOUR, ONE JOB (20 Aug 2026) ───────────────────────────────────────
   --amber used to carry seven unrelated jobs, so a coloured number in BenchBear
   predicted nothing and every one of them had to be decoded. The split, scheme B
   off the contact sheet:
     --amber   IDENTITY ONLY — the Upper session tile, the History workout rail
               and title. It sits beside violet / teal / blue and means "this is
               a thing of that kind". It never states a value again.
     --red     ATTENTION — weight or waist up against last week, a macro missed,
               a backup overdue. One colour, one meaning: this went the wrong way.
     --green   done / on target. Unchanged.
     --accent  you can tap this. Unchanged.
     --muted   no verdict. A state that is neither good nor bad takes no colour —
               see .gv-soft below, which is the whole reason the rule needs saying.
   Before adding a colour user anywhere, check what that token already means:
     grep -c -- "var(--X)" css/style.css                                        */

* { box-sizing: border-box; margin: 0; padding: 0; -webkit-tap-highlight-color: transparent; }
html, body { background: var(--bg); color: var(--text); font-family: 'DM Sans', sans-serif; font-size: 16px; }
/* `overflow: hidden` only. `touch-action: none` used to sit here too, and on an iOS PWA that is a
   root-level suppression of default touch behaviour for every descendant — including the two inputs
   you have to use to get in. Paired with the scroll handler below (which snapped the page back to 0
   whenever iOS scrolled the focused field up above the keyboard) it made the login screen genuinely
   hard to operate on a phone, which is what "I can't log in without swiping the app off" was.
   Nothing is lost by dropping it: #login-screen is `position: fixed; inset: 0` over an opaque
   background, so there is no page behind it to scroll or rubber-band into view. */
html.login-active { overflow: hidden; }

/* overflow-y:auto and `margin: auto` on the card, together, since 24 Aug: the reset panel is a
   code box plus two password boxes plus a heading, and on a small phone with the keyboard up it is
   taller than what is left of the screen. A centred flex item that overflows gets clipped at the
   TOP — the half you need — and `align-items: center` alone cannot scroll to it. `margin: auto`
   centres it while it fits and lets it scroll once it doesn't. html.login-active still has
   overflow:hidden, so this is the only thing on the screen that scrolls. */
#login-screen { position: fixed; inset: 0; z-index: 999; display: flex; align-items: center; justify-content: center; padding: 2rem; background: var(--bg); overflow-y: auto; }
.login-card { width: 100%; max-width: 380px; margin: auto; }
.login-mark { display: block; color: var(--accent); margin-bottom: 10px; }
.login-title { font-family: 'Bebas Neue', sans-serif; font-size: 52px; letter-spacing: 2px; line-height: 1; margin-bottom: 4px; }
.login-sub { font-size: 13px; color: var(--muted); margin-bottom: 2.5rem; }
.form-group { margin-bottom: 1rem; }
.form-group label { font-size: 11px; color: var(--muted); text-transform: uppercase; letter-spacing: 1px; display: block; margin-bottom: 6px; }
.form-group input { width: 100%; background: var(--bg2); border: 1px solid var(--border2); border-radius: 8px; padding: 14px 16px; color: var(--text); font-size: 15px; font-family: 'DM Sans', sans-serif; outline: none; transition: border-color 0.2s; }
.form-group input:focus { border-color: var(--accent); }
.btn-primary { width: 100%; background: var(--accent); border: none; border-radius: 8px; padding: 14px; color: #fff; font-size: 15px; font-weight: 600; font-family: 'DM Sans', sans-serif; cursor: pointer; margin-top: 0.5rem; transition: background 0.2s, transform 0.1s; }
.btn-primary:active { transform: scale(0.98); background: #933f1c; }
.login-error { font-size: 13px; color: var(--red); margin-top: 0.75rem; text-align: center; display: none; }
.login-diag { font-family: 'DM Mono', monospace; font-size: 10px; line-height: 1.5; color: var(--muted); margin-top: 1.25rem; text-align: center; letter-spacing: 0.02em; word-break: break-word; }
.login-diag.warn { color: var(--red); }

/* The reset flow's own furniture. A <button>, not an <a>: there is nowhere to navigate to, and an
   anchor with href="#" on an iOS PWA is a way to lose the whole app to a stray navigation. */
.login-link { display: block; width: 100%; background: none; border: none; color: var(--muted); font-family: 'DM Sans', sans-serif; font-size: 13px; text-align: center; padding: 14px 0 0; cursor: pointer; text-decoration: underline; text-underline-offset: 3px; }
.login-link:disabled { opacity: 0.55; cursor: default; text-decoration: none; }
.login-step { font-family: 'Bebas Neue', sans-serif; font-size: 24px; letter-spacing: 1px; line-height: 1.1; margin-bottom: 6px; }
.login-note { font-size: 13px; color: var(--muted); line-height: 1.5; margin-bottom: 1.5rem; }
/* The signup consent box. A bare checkbox, not a .form-group input, which would stretch it to 100%. */
.consent-row { display: flex; align-items: flex-start; gap: 10px; font-size: 13px; color: var(--muted); line-height: 1.5; margin: 0 0 1.25rem; text-align: left; cursor: pointer; }
.consent-row input { flex: none; width: 20px; height: 20px; margin: 1px 0 0; accent-color: var(--accent); cursor: pointer; }
.consent-row a { color: var(--text); text-underline-offset: 3px; }
a.account-link { display: inline-block; }
/* The account field is there to tell the password manager which login changed, so it must be a real
   focusable input — but it is not something to type in, and it should not look like one. */
.form-group input[readonly] { color: var(--muted); cursor: default; }
.form-group input[readonly]:focus { border-color: var(--border2); }
/* `.form-group input` is (0,1,1), so a bare `.code-input` would lose to it and the digits would
   come out at 15px like any other field. It has to be qualified to win. */
/* Sized for the longest code GoTrue can be set to emit (10), not the six it was written for. */
.form-group input.code-input { font-family: 'DM Mono', monospace; font-size: 22px; letter-spacing: 7px; text-align: center; padding-left: 23px; }

/* A flex column, not a block, since 19 Aug: it lets a page grow to fill whatever is left of the
   screen, which is what parks Home's account/build footer at the bottom instead of leaving it
   stranded under the last button. padding-bottom clears the fixed nav. The topbar is still
   position:sticky — sticky works on a flex item, the scroll container is the page either way. */
#app { display: flex; flex-direction: column; min-height: 100vh; padding-bottom: 80px; }
.topbar { position: sticky; top: 0; z-index: 100; background: var(--bg); border-bottom: 1px solid var(--border); padding: 0.75rem 1.25rem; display: flex; align-items: center; justify-content: space-between; }
.topbar-title { font-family: 'Bebas Neue', sans-serif; font-size: 22px; letter-spacing: 1px; color: var(--accent); cursor: pointer; display: flex; align-items: center; gap: 8px; line-height: 1; }
/* The brand mark, 29 Sept 2026: a bear paw whose pad is a weight plate (B3 off the mark sheet). Inline SVG on
   currentColor, so it follows the wordmark's colour everywhere it sits, the dark Ghost bar included. */
.brand-mark { flex: none; display: block; }
.logout-btn { font-size: 12px; color: var(--muted); background: none; border: none; cursor: pointer; padding: 4px 8px; }
/* Signed in as another account (28 Sept 2026). The topbar is already on every screen and already
   sticky, so it carries the warning rather than a second bar eating height: it goes ink-dark, says
   whose account this is, and logout becomes Exit. Nothing else in the app is dark, so it cannot be
   mistaken for any other state. */
.imp-as { display: none; flex: 1; min-width: 0; margin: 0 10px; font-size: 12px; color: var(--bg2); text-align: center; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.imp-as b { font-weight: 700; }
body.impersonating .topbar { background: var(--text); border-bottom-color: var(--text); }
body.impersonating .imp-as { display: block; }
body.impersonating .logout-btn { color: var(--text); background: var(--bg2); border-radius: 7px; font-weight: 700; padding: 6px 12px; }
body.impersonating .no-impersonate { display: none; }
.imp-row { display: flex; justify-content: space-between; align-items: center; gap: 10px; width: 100%; padding: 12px 4px; background: none; border: none; border-bottom: 1px solid var(--border); text-align: left; cursor: pointer; font-family: 'DM Sans', sans-serif; color: var(--text); }
.imp-row:last-child { border-bottom: none; }
.imp-row > div { min-width: 0; }
.imp-row-name { font-size: 15px; font-weight: 600; }
.imp-row-sub { font-size: 12px; color: var(--muted); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.imp-row-when { font-size: 12px; color: var(--muted2); white-space: nowrap; }
.imp-empty { font-size: 13px; color: var(--muted); padding: 8px 4px; }

.bottom-nav { position: fixed; bottom: 0; left: 0; right: 0; z-index: 100; background: var(--bg2); border-top: 1px solid var(--border); display: flex; height: 65px; }
.nav-item { flex: 1; display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 3px; background: none; border: none; color: var(--muted); cursor: pointer; transition: color 0.2s; font-family: 'DM Sans', sans-serif; }
.nav-item.active { color: var(--accent); }
.nav-item svg { width: 20px; height: 20px; }
.nav-label { font-size: 10px; }

.page { display: none; padding: 1rem; }
/* Phone-first, and until 20 Aug 2026 literally only phone-shaped: on a desktop window every page
   stretched to the full width of the browser. The History check-in card is a five-column grid whose
   label column is `1fr`, so at 2,000px wide the label sat on the left and its value and change
   columns ended up a whole screen away on the right — Del read that as the "vs" being broken. It
   was not broken, it was two thousand pixels to the right of the word it belonged to.
   Capping the reading width makes a wide window show the same layout the phone does, centred. */
.page.active { display: block; flex: 1 1 auto; width: 100%; max-width: 520px; margin: 0 auto; }

.section-title { font-family: 'Bebas Neue', sans-serif; font-size: 26px; letter-spacing: 1px; margin-bottom: 0.75rem; }
.section-sub { font-size: 13px; color: var(--muted); margin-top: -0.5rem; margin-bottom: 0.75rem; }
.card { background: var(--bg2); border: 1px solid var(--border); border-radius: 12px; padding: 1rem; margin-bottom: 0.875rem; }
.card-title { font-size: 12px; font-weight: 600; color: var(--muted2); text-transform: uppercase; letter-spacing: 0.5px; margin-bottom: 0.875rem; }

/* The Stats page label system: exactly two levels, distinguished by weight and brightness rather
   than by size, so the step between them is deliberate instead of accidental.
   L1 .stats-section — header OUTSIDE a box, governs everything to the next one. Brighter, bolder.
   L2 .stat-label    — label INSIDE a box, names that one number. Dimmer, regular.
   Both 11px on purpose: four slightly-different sizes were what made this page look untidy. */
.stats-section { font-size: 11px; font-weight: 600; color: var(--muted2); text-transform: uppercase; letter-spacing: 1px; margin: 1.125rem 0 0.5rem; }
.section-title + .stats-section { margin-top: 0; }
.stat-label { font-size: 11px; font-weight: 400; color: var(--muted); text-transform: uppercase; letter-spacing: 0.5px; margin-bottom: 4px; }

.stat-unit { font-family: 'DM Sans', sans-serif; font-size: 12px; color: var(--muted); margin-left: 3px; letter-spacing: 0; }
.stat-green { color: var(--green); } .stat-accent { color: var(--accent); }

.field-group { margin-bottom: 0.875rem; }
.field-label { font-size: 11px; color: var(--muted); text-transform: uppercase; letter-spacing: 0.5px; display: block; margin-bottom: 5px; }
.field-input { width: 100%; background: var(--bg3); border: 1px solid var(--border2); border-radius: 8px; padding: 11px 13px; color: var(--text); font-size: 15px; font-family: 'DM Sans', sans-serif; outline: none; transition: border-color 0.2s; }
.field-input:focus { border-color: var(--accent); }
textarea.field-input { resize: vertical; min-height: 70px; }
input[type="date"].field-input { -webkit-appearance: none; appearance: none; color-scheme: dark; }
input[type="time"].field-input { -webkit-appearance: none; appearance: none; color-scheme: dark; }

/* "Weighed at 07:12" under the weight box. Hidden until there is a weight to timestamp, so a day
   with no weighing keeps the form exactly as short as it was. */
.weigh-time { display: flex; align-items: center; gap: 8px; margin-top: 6px; }
.weigh-time-label { font-size: 10px; color: var(--muted); text-transform: uppercase; letter-spacing: 0.5px; white-space: nowrap; margin: 0; }
.field-input-time { width: auto; flex: 1; padding: 7px 9px; font-size: 13px; }

/* Cardio field rows sit in a grid, one field per cell. A label that wraps to two lines
   ("DURATION (MIN)" next to "INCLINE (%)") pushed its input down out of line with the
   others. Grid stretches the cells to equal height, so making each cell a column and
   letting the input take the leftover space above it keeps every input on one line. */
.cardio-field-grid .field-group { display: flex; flex-direction: column; margin-bottom: 0; }
.cardio-field-grid .field-input { margin-top: auto; }


.session-grid { display: grid; grid-template-columns: repeat(2, 1fr); gap: 8px; margin-bottom: 0.875rem; }
.session-btn { position: relative; background: var(--bg3); border: 1px solid var(--border2); border-radius: 10px; padding: 0.875rem; text-align: left; cursor: pointer; transition: all 0.15s; }
.session-edit-btn { position: absolute; top: 6px; right: 6px; width: 28px; height: 28px; border-radius: 50%; border: 1px solid var(--accent); background: var(--bg2); color: var(--accent); font-size: 13px; cursor: pointer; display: flex; align-items: center; justify-content: center; -webkit-tap-highlight-color: transparent; }
.session-edit-btn:active { background: color-mix(in srgb, var(--accent) 16%, transparent); }
.session-btn.selected { border-color: var(--accent); background: color-mix(in srgb, var(--accent) 10%, transparent); }
.session-btn.done { border-color: var(--green); background: color-mix(in srgb, var(--green) 8%, transparent); }
.session-name { font-size: 14px; font-weight: 600; color: var(--text); }
.session-focus { font-size: 11px; color: var(--muted2); margin-top: 2px; }

/* ─── SESSION / PROGRAMME TILE COLOUR (17 Aug 2026) ────────
   The picker was seven identical grey boxes. The colour here is IDENTITY — what you're training
   (upper / lower / full body / CV) on the session screen, which programme on the screen above it.
   It is never state.

   Which is why `done` no longer repaints the tile (19 Aug 2026). It used to overwrite --sc with
   green, and that broke the rule this block is built on: Lower B went from violet to green for one
   day and stopped looking like the tile you had learned to find. Identity that changes with state
   isn't identity. State is now carried by the green "✓ logged today" line alone — one tick, on a
   tile that still looks like itself.
   One custom property drives the rail, the name and the tint, so a new session type is one line
   rather than four rules. Only `.tinted` tiles get any of it — the ← Programmes tile stays plain. */
.session-btn.tinted { padding-left: 1rem; background: var(--sc-bg); border-color: var(--sc-bd); }
.session-btn.tinted::before {
  content: ''; position: absolute; left: 0; top: 0; bottom: 0; width: 3px;
  background: var(--sc); border-radius: 10px 0 0 10px;
}
.session-btn.tinted .session-name {
  font-family: 'Bebas Neue', sans-serif; font-size: 20px; letter-spacing: 0.5px;
  line-height: 1.05; color: var(--sc);
}
/* Violet and teal are new. Blue, green, amber and accent were all already carrying a meaning. */
.sc-upper { --sc: var(--amber);  --sc-bg: color-mix(in srgb, var(--amber) 6%, transparent);  --sc-bd: color-mix(in srgb, var(--amber) 32%, transparent); }
.sc-lower { --sc: var(--violet); --sc-bg: color-mix(in srgb, var(--violet) 6%, transparent); --sc-bd: color-mix(in srgb, var(--violet) 32%, transparent); }
.sc-full  { --sc: var(--teal);   --sc-bg: color-mix(in srgb, var(--teal) 6%, transparent);  --sc-bd: color-mix(in srgb, var(--teal) 32%, transparent); }
.sc-cv    { --sc: var(--blue);   --sc-bg: color-mix(in srgb, var(--blue) 6%, transparent);  --sc-bd: color-mix(in srgb, var(--blue) 32%, transparent); }
.sc-open  { --sc: var(--violet); --sc-bg: color-mix(in srgb, var(--violet) 6%, transparent); --sc-bd: color-mix(in srgb, var(--violet) 32%, transparent); }
.sc-own   { --sc: var(--muted2); --sc-bg: color-mix(in srgb, var(--text) 2%, transparent); --sc-bd: var(--border2); }
/* The folder, not the muscle — these two are the screen above. */
.sc-prog-upper-lower  { --sc: var(--blue); --sc-bg: color-mix(in srgb, var(--blue) 6%, transparent); --sc-bd: color-mix(in srgb, var(--blue) 32%, transparent); }
.sc-prog-the-reveal   { --sc: var(--amber); --sc-bg: color-mix(in srgb, var(--amber) 6%, transparent); --sc-bd: color-mix(in srgb, var(--amber) 32%, transparent); }
.sc-prog-full-body-cv { --sc: var(--teal); --sc-bg: color-mix(in srgb, var(--teal) 6%, transparent); --sc-bd: color-mix(in srgb, var(--teal) 32%, transparent); }
/* The one state that still repaints a tile, and only for the instant between tapping it and the
   logger opening — it's the tap landing, not a status. Higher specificity than `.session-btn.tinted`
   so it wins regardless of source order; don't rely on the ordering. */
.session-btn.tinted.selected { --sc: var(--accent); --sc-bg: color-mix(in srgb, var(--accent) 12%, transparent);   --sc-bd: var(--accent); }

/* When you last did this one. Quiet and monospaced — it's context, not the thing you tapped for. */
.session-last { font-family: 'DM Mono', monospace; font-size: 10px; color: var(--muted); margin-top: 7px; }
/* The only thing that marks a logged session now, so it carries a little more weight than the
   "last · 15 Aug" line it replaces. */
.session-last.done { color: var(--green); font-family: 'DM Sans', sans-serif; font-size: 11px; font-weight: 600; }

/* Both span the full grid rather than taking a cell: neither is a session, and a half-width
   "← Programmes" reads as a fifth thing you could train. */
.session-grid .grid-full { grid-column: 1 / -1; }
.prog-band {
  grid-column: 1 / -1; display: flex; align-items: center; gap: 10px;
  border: 1px solid var(--sc-bd); background: var(--sc-bg); border-radius: 10px; padding: 9px 12px;
}
.prog-band-bar { width: 3px; align-self: stretch; border-radius: 2px; background: var(--sc); }
.prog-band-name { font-family: 'Bebas Neue', sans-serif; font-size: 17px; letter-spacing: 0.5px; line-height: 1; color: var(--sc); }
.prog-band-count { margin-left: auto; font-family: 'DM Mono', monospace; font-size: 10px; color: var(--muted); }

/* position + overflow are for the overload panel at the bottom of this file: the panel is absolutely
   positioned against this block and parks off its right edge when closed. Nothing else inside a
   block is absolutely positioned or wants to overflow it — the superset and variation pickers are
   both normal-flow and push the block taller. */
.exercise-block { background: var(--tile); border: 1px solid var(--border2); border-radius: 10px; padding: 0.875rem; margin-bottom: 0.75rem; position: relative; overflow: hidden; }
.ex-top { margin-bottom: 8px; }
.ex-name-display { font-size: 15px; font-weight: 600; color: var(--text); line-height: 1.3; margin-bottom: 4px; }
.ex-pills { display: flex; align-items: center; gap: 6px; flex-wrap: wrap; margin-bottom: 6px; }
.ex-note-text { font-size: 11px; color: var(--muted); }

/* THREE COLUMNS, NOT FOUR (7 Sept 2026). The row used to end in a 72px `.prev-badge` carrying last
   time's "70×7" — on every set, of every exercise, of every session. Both inputs gain about a third
   of their width back, which is what the change is for: they are tapped with a thumb, in a gym,
   between sets. Those numbers now live in the overload panel at the bottom of this file.
   The History edit modal keeps its fourth column and says so — see .has-prev. */
.set-row { display: grid; grid-template-columns: 18px 1fr 1fr; gap: 6px; margin-bottom: 6px; align-items: center; }
.set-row.has-prev { grid-template-columns: 18px 1fr 1fr 72px; }
.set-num { font-size: 12px; color: var(--muted); font-family: 'DM Mono', monospace; text-align: center; }
.set-input { width: 100%; background: var(--bg2); border: 1px solid var(--border2); border-radius: 6px; padding: 9px 6px; color: var(--text); font-size: 14px; font-family: 'DM Mono', monospace; outline: none; text-align: center; transition: border-color 0.2s; min-width: 0; }
.set-input:focus { border-color: var(--accent); }
.set-input::placeholder { color: var(--border2); font-size: 12px; }
/* The optional-weight cell — pull ups / dips / deadhangs / farmers walks, which can be done at
   bodyweight or loaded. See bwCellHtml() in js/app.js for why this exists at all. The pill and the
   input share one grid slot and only one is ever visible, so the column never changes width when it
   flips. DASHED border, where a fixed .set-label is solid: dashed means "you can change this",
   solid means "this exercise has no weight to enter". */
.bw-cell { min-width: 0; }
.bw-cell > [hidden] { display: none; }
.bw-pill { width: 100%; background: var(--bg2); border: 1px dashed var(--border2); border-radius: 6px; padding: 9px 6px; color: var(--muted); font-size: 12px; font-family: 'DM Sans', sans-serif; letter-spacing: 0.5px; text-align: center; cursor: pointer; -webkit-appearance: none; appearance: none; }
.bw-pill:active { border-color: var(--accent); color: var(--text); }
.set-label { width: 100%; background: var(--bg2); border: 1px solid var(--border2); border-radius: 6px; padding: 9px 6px; color: var(--muted); font-size: 12px; font-family: 'DM Sans', sans-serif; text-align: center; min-width: 0; display: flex; align-items: center; justify-content: center; }
.prev-badge {
  font-size: 10px;
  color: var(--muted);
  font-family: 'DM Mono', monospace;
  background: var(--bg2);
  border-radius: 4px;
  padding: 2px 5px;
  white-space: nowrap;
  width: 64px;                /* fixed width so every row lines up */
  text-align: center;
  justify-self: end;          /* hug the right edge of the grid column */
}

/* Wraps (28 Sept 2026, C30). A lift can now carry any number of variations, and a fixed row squeezed
   five into phone width: labels broke mid-word and the fifth was cut off. Each button keeps its label
   on one line and the row wraps only when they genuinely do not fit, so a two- or three-way toggle is
   exactly as tall as before. */
.variation-toggle { display: flex; flex-wrap: wrap; gap: 6px; margin-bottom: 10px; }
.var-btn { flex: 1 1 auto; min-width: 64px; white-space: nowrap; padding: 7px 8px; background: var(--bg2); border: 1px solid var(--border2); border-radius: 6px; color: var(--muted); font-size: 12px; cursor: pointer; transition: all 0.15s; text-align: center; font-family: 'DM Sans', sans-serif; display: flex; align-items: center; justify-content: center; }
.var-btn.selected { border-color: var(--accent); background: color-mix(in srgb, var(--accent) 15%, transparent); color: var(--accent); font-weight: 600; }

.pill { display: inline-block; font-size: 11px; padding: 3px 8px; border-radius: 20px; }
.pill-rest { background: color-mix(in srgb, var(--text) 5%, transparent); color: var(--muted2); }

/* ─── THE EXERCISE META ROW (E2, rebuilt 27 Aug 2026, proof sheet cut 23) ──────
   Three tinted pills in three colours became a segmented control and two tags in TWO colours, and
   which two is the whole point — see the comment on setsStepperHtml() in app.js:
     --accent  the control, and nothing else in the block. The tinted thing is the pressable thing.
     --sc      the session (amber/violet/teal), inherited from the sessionColourClass on
               .exercise-block. It states; it never invites a tap.
   The old row used --blue for the count and --green for the rep range. Blue already carried
   superset, programme and logging; green means DONE, and a rep range is not a completed thing.
   Before adding a colour user here, check what that token already means:
     grep -c -- "var(--X)" css/style.css                                                          */
.ex-tag {
  display: inline-flex; align-items: center; gap: 5px;
  background: color-mix(in srgb, var(--sc, var(--muted2)) 15%, transparent);
  color: var(--sc, var(--muted2));
  border-radius: 999px; padding: 6px 11px;
  font-size: 10.5px; font-weight: 700; letter-spacing: 0.04em;
  font-variant-numeric: tabular-nums;
}
.ex-tag svg { width: 11px; height: 11px; flex: none; }

/* The count IS the add/remove-set control (24 Aug 2026), now as a segmented control: a sunken track
   with the count on a raised thumb. Same shape as the Stats flip switch, so the app has one idea of
   what "pick a value" looks like. The 30px ends are the tap targets — a whole end each, not a glyph
   floating inside one — and the thumb's min-width stops the track resizing as 3 sets becomes 4. */
/* ⚠️ THE TRACK IS NEUTRAL AND MUST STAY NEUTRAL (27 Aug 2026). It shipped tinted in --accent for
   one build, on the reasoning that terracotta means "you can tap this" — and on the logger that put
   THREE accent boxes on one screen, the session pill, the reorder bar and this, of which only this
   one is about the exercise you are looking at. Del: "blend it into the background color".
   What says it is a control now is the shape, not a colour: a sunken track with the count on a
   raised thumb. The only colour left on this row is the session's, on the two tags. */
.sets-seg {
  display: inline-flex; align-items: center; gap: 2px;
  background: color-mix(in srgb, var(--text) 6%, transparent);
  border-radius: 10px; padding: 3px;
}
.sets-seg .sets-step {
  width: 30px; height: 30px; padding: 0;
  border: none; background: none; color: var(--muted2);
  border-radius: 8px;
  font-family: 'DM Sans', sans-serif; font-size: 17px; line-height: 1; cursor: pointer;
  -webkit-tap-highlight-color: transparent;
}
.sets-seg .sets-step:active { background: color-mix(in srgb, var(--text) 8%, transparent); }
.sets-seg .sets-step-count {
  min-width: 56px; height: 30px;
  display: flex; align-items: center; justify-content: center;
  background: var(--bg2); border-radius: 8px;
  box-shadow: 0 1px 3px rgba(35, 31, 26, 0.18);
  color: var(--text);
  font-size: 11.5px; font-weight: 700; letter-spacing: 0.04em; text-transform: uppercase;
  white-space: nowrap;
}
/* One set left: removeOpenSetRow is a no-op, so the − says so rather than failing silently. */
.sets-seg.at-min .sets-step:first-child { opacity: 0.3; }

/* ─── WARM-UP SETS (8 Sept 2026) ──────────────────────────────────────────────
   Del's ask: "i need warm up sets". Picked over four rounds of contact sheets — the control (cut C
   of fourteen), what it displaced (cut 9 of twelve), and the highlight (cut C of seven).

   THE SECOND STEPPER. Same object as .sets-seg, same neutral track, one thumb-width away: adding a
   warm-up is the same gesture as adding a set. The thumb is wider because "2 warm-up" is a longer
   word than "3 sets" and a track that resized as the count changed would jitter.
   ⚠️ At zero it reads "warm-up", not "0 warm-up" — see warmupStepperHtml(). Every exercise in the
   app opens with none, so a column of zeros would be the loudest thing on the logger. */
.sets-seg.seg-warm .sets-step-count { min-width: 74px; }

/* WHAT THE SECOND STEPPER DISPLACED. The rep target and rest used to be two .ex-tag pills on this
   row; two steppers and two pills is 416px of content in a 330px row. They are a caption now, not
   two objects — see targetRestLineHtml(). The session colour comes off them entirely, which is the
   one thing to check before "restoring" it: --sc is still doing its job on the tile, in the panel's
   BEAT column, and a caption is not a thing that states an identity. */
.ex-target-line {
  font-size: 10px; letter-spacing: 0.03em; color: var(--muted);
  font-variant-numeric: tabular-nums; margin-bottom: 6px;
}
.ex-target-line b { color: var(--muted2); font-weight: 600; }

/* ⭐ THE WARM-UP ROW WEARS THE SELECTED-VARIATION TREATMENT IN THE SESSION'S COLOUR.
   Del picked it by pointing at the Smith button: "can we not do something similar to Smith
   highlighted for the warm up sets? i need highlights". That treatment is four things at once —
   tinted fill, matching border, matching text, 600 weight — and the only question was which token
   it is drawn in. He was shown it in terracotta, in the session colour, and in neutral grey.

   ⚠️ IT IS --sc AND NOT --accent, AND THAT IS THE WHOLE REASON IT IS ALLOWED TO EXIST.
   Terracotta means "you can tap this" — the selected variation, the overload tab, the focused input
   border. Spending it on a category would leave the logger with no colour that means "pressable".
   --sc states what kind of thing you are looking at and never invites a tap, which is exactly the
   job here, and it means a warm-up reads amber on an Upper day and violet on a Lower one.

   ⛔ DO NOT ADD A SECOND SIGNAL. Round three drew seven of them — a chip, a rail, an indent, a
   hairline, shorter rows — and all seven were rejected for being too quiet. This is the loud
   version; anything stacked on top of it is the clutter that got three panel rounds thrown out. */
.set-row.warm-row .set-num { color: var(--sc); font-weight: 600; }
.set-row.warm-row .set-input,
.set-row.warm-row .set-label {
  border-color: color-mix(in srgb, var(--sc) 55%, transparent);
  background: color-mix(in srgb, var(--sc) 15%, transparent);
  color: var(--sc);
}
/* The focus ring still wins: the box you are typing in is terracotta whatever kind of row it is on,
   because that is the one thing --accent has always meant. */
.set-row.warm-row .set-input:focus { border-color: var(--accent); }
.set-row.warm-row .set-input::placeholder { color: color-mix(in srgb, var(--sc) 45%, transparent); }

/* ─── DROP SETS (28 Sept 2026) ────────────────────────────────────────────────
   Picked off a contact sheet as D + L4 slim: the warm-up treatment (tinted fill, matching border)
   drawn in NEUTRAL GREY on a shorter row. The session colour already means "warm-up" on this tile
   and terracotta means "you can tap this", so a drop takes no colour. It is marked by where it sits
   (under the last set, above its rest line) and by the ↘. The digits stay --text: they are his
   numbers, and a drop is not a figure to fade.
   ⚠️ The ↘ cell is a button, but it only acts on the last drop while that drop is empty. Then it
   reads ✕ in terracotta: a mis-tap on ↘ Drop set is undone where it landed. */
.set-row.drop-row { margin-top: -2px; }
.set-row.drop-row .set-input {
  padding: 5px 6px; font-size: 13px;
  border-color: color-mix(in srgb, var(--muted2) 45%, transparent);
  background: color-mix(in srgb, var(--muted2) 10%, transparent);
}
.set-row.drop-row .set-input:focus { border-color: var(--accent); }
.set-row.drop-row .set-input::placeholder { color: color-mix(in srgb, var(--muted2) 55%, transparent); }
.drop-mark {
  background: none; border: none; padding: 0; height: 100%; min-height: 28px;
  font-family: 'DM Mono', monospace; font-size: 13px; font-weight: 600; color: var(--muted2);
  pointer-events: none; -webkit-tap-highlight-color: transparent;
}
/* \FE0E = the text arrow. Without it iOS may draw ↘ as a blue emoji tile. */
.drop-mark::before { content: '\2198\FE0E'; }
.drop-row.removable .drop-mark { pointer-events: auto; cursor: pointer; color: var(--accent); }
.drop-row.removable .drop-mark::before { content: '✕'; font-size: 11px; }
/* A tapped-in drop settles into place rather than appearing: it is the only row on the tile that
   arrives mid-set, and the eye should follow it down from the button to the rows. */
.drop-row.drop-in { animation: drop-in 180ms ease-out; }
@keyframes drop-in { from { opacity: 0; transform: translateY(-6px); } to { opacity: 1; transform: none; } }
@media (prefers-reduced-motion: reduce) { .drop-row.drop-in { animation: none; } }

/* ─── ADD TO THIS SESSION — EXERCISE | CARDIO (E24, 31 Aug 2026) ──────────────
   The same sunken-track/raised-thumb shape as .sets-seg above, on purpose: the app should have one
   idea of what "pick between two things" looks like. It replaced two identical cards four
   millimetres apart, which Del kept mixing up — see renderAddToSessionRow() for the argument.

   ⚠️ THIS THUMB IS TINTED AND THE SETS TRACK ABOVE IS NOT. That is a deliberate split, not drift.
   The sets control lives inside an exercise block where a tint made it the third terracotta box on
   one screen ("blend it into the background color", 27 Aug); this one sits alone above Session
   Notes with nothing to compete with, and Del chose the tint off a contact sheet knowing that.
   If anything else accent-coloured ever lands down there, this goes back to neutral. */
.add-seg {
  display: flex; align-items: center; gap: 2px;
  background: color-mix(in srgb, var(--text) 6%, transparent);
  border-radius: 10px; padding: 3px; margin-bottom: 8px;
}
.add-seg-opt {
  flex: 1; height: 30px;
  display: flex; align-items: center; justify-content: center;
  border: none; background: none; border-radius: 8px;
  font-family: 'DM Sans', sans-serif; font-size: 11px; font-weight: 700;
  letter-spacing: 0.04em; text-transform: uppercase; color: var(--muted2);
  cursor: pointer; -webkit-tap-highlight-color: transparent;
}
.add-seg-opt.active {
  background: color-mix(in srgb, var(--accent) 14%, var(--bg2));
  color: var(--accent);
  box-shadow: 0 1px 3px rgba(35, 31, 26, 0.18);
}
.add-seg-opt:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
/* The two pickers share the card and swap on the switch. Belt and braces over the UA's [hidden]
   rule: .field-input sets no display of its own today, but it is one edit away from doing so. */
.field-input[hidden] { display: none; }
.pill-accent { background: color-mix(in srgb, var(--accent) 15%, transparent); color: var(--accent); font-weight: 600; }

/* One-line macro entry on the check-in (18 Aug 2026). Set apart from the ordinary fields with a
   left rule, because it is a shortcut INTO them rather than another thing to fill in — everything
   it does lands in the boxes below it. */
.macroline { border-left: 2px solid var(--accent); padding-left: 10px; }
.macroline-hint { font-size: 10.5px; color: var(--muted); margin-top: 5px; line-height: 1.4; }
.macroline-echo { font-family: "DM Mono", monospace; font-size: 11.5px; margin-top: 5px; min-height: 15px; }
.macroline-echo.hit  { color: var(--green); }
.macroline-echo.miss { color: var(--muted2); }

.btn { border-radius: 8px; padding: 12px 20px; font-size: 14px; font-weight: 600; font-family: 'DM Sans', sans-serif; cursor: pointer; transition: all 0.15s; border: none; }
.btn-full { width: 100%; }
.btn-save { background: var(--accent); color: #fff; }
.btn-save:active { background: #933f1c; transform: scale(0.98); }
.btn-outline { background: transparent; border: 1px solid var(--border2); color: var(--text); }
.btn-outline:active { background: var(--bg3); }

.week-strip { display: flex; gap: 5px; margin-bottom: 0.875rem; }
.week-day { flex: 1; background: var(--bg3); border-radius: 8px; padding: 7px 3px; text-align: center; border: 1px solid var(--border); }
.week-day.done { border-color: var(--green); background: color-mix(in srgb, var(--green) 10%, transparent); }
.week-day.today { border-color: var(--accent); background: color-mix(in srgb, var(--accent) 10%, transparent); }
.week-day.rest { opacity: 0.35; }
.wd-name { font-size: 9px; color: var(--muted); text-transform: uppercase; letter-spacing: 0.3px; margin-bottom: 4px; }
.wd-dot { width: 7px; height: 7px; border-radius: 50%; background: var(--border2); margin: 0 auto; }
.week-day.done .wd-dot { background: var(--green); }
.week-day.today .wd-dot { background: var(--accent); }
/* .wd-session went with shortSessionLabel() on 1 Sept 2026 (C18) — the strip carries no session name
   now, so every day shows its dot and the tile's colour says whether it was trained. */
/* Today, once it's been trained: the accent border still marks which day you're on, but the dot
   goes green like every other completed day. Before this, today never showed as done. */
.week-day.today.done .wd-dot { background: var(--green); }

/* The overdue-backup line on Home. Hidden by default and shown by renderBackupPrompt(), so a stale
   cache or a JS failure leaves no empty box behind. Red since 20 Aug: it was amber on the
   grounds that nothing is broken yet, but amber is the identity colour now and a nine-day-old
   backup is the one thing on Home that wants acting on. */
.backup-nudge { display: none; align-items: center; gap: 8px; margin-bottom: 0.875rem; padding: 10px 12px; background: color-mix(in srgb, var(--red) 10%, transparent); border: 1px solid var(--red); border-radius: 10px; font-size: 13px; font-weight: 600; color: var(--red); cursor: pointer; }
.backup-nudge::before { content: '⤓'; font-size: 15px; }
.backup-nudge:active { background: color-mix(in srgb, var(--red) 20%, transparent); }

.history-date { font-size: 11px; color: var(--muted); margin-bottom: 3px; }
.history-stats { display: flex; gap: 6px; flex-wrap: wrap; }

.toast { position: fixed; bottom: 85px; left: 50%; transform: translateX(-50%); background: var(--bg3); border: 1px solid var(--border2); border-radius: 8px; padding: 10px 18px; font-size: 13px; color: var(--text); z-index: 999; opacity: 0; transition: opacity 0.3s; pointer-events: none; white-space: nowrap; }
.toast.show { opacity: 1; }
.toast.success { border-color: var(--green); color: var(--green); }
.toast.error { border-color: var(--red); color: var(--red); }

.loading { text-align: center; color: var(--muted); font-size: 13px; padding: 1.5rem; }
.empty { text-align: center; padding: 1.5rem 1rem; color: var(--muted); font-size: 13px; }

/* LANDING PAGE
   Home is the one page with a footer that belongs at the foot of the screen rather than after the
   content, so it lays itself out as a column and .landing-foot eats the leftover height. Everything
   here is also spaced wider than the rest of the app (19 Aug) — with all the blocks on 1.25rem the
   stack read as one solid slab, and Home is a glance, not a form. */
#page-home.active { display: flex; flex-direction: column; }
#page-home > .daily-quote { margin-bottom: 1.5rem; }
#page-home > .next-up,
#page-home > .landing-stats,
#page-home > .landing-stats-2,
#page-home > .backup-nudge,
#page-home > .week-strip { margin-bottom: 1.5rem; }
#page-home > .landing-actions { margin-bottom: 0; }
.landing-foot { margin-top: auto; padding-top: 2rem; }

.landing-greeting { font-family: 'DM Sans', sans-serif; font-size: 28px; font-weight: 600; letter-spacing: 0; margin-bottom: 2px; }
.landing-date { font-size: 13px; color: var(--muted); margin-bottom: 1.25rem; }

/* NEXT UP — where you are in the rolling cycle, on the screen you open on the way to the gym.
   It wears the session's own identity colour through the same --sc contract the workout tiles use
   (see CODEBASE.md "Session and programme tile colour"), so Upper B is the same amber here as it
   is on the picker. The sc-* classes only declare custom properties, which is what makes them
   reusable outside .session-btn. Colour stays identity: nothing here paints state. */
/* Two rows, not four (27 Aug 2026, proof sheet cut 11) — Del asked for it shorter and this is the
   same four facts folded into a name, one sub-line and a button-looking span. The name is 26px
   rather than 34: at 34 it was the largest thing on Home, which is a lot of shout for a card whose
   job is "tap here". `.next-up-go` only LOOKS like a button — the card owns the click. */
.next-up { display: none; position: relative; overflow: hidden; background: var(--sc-bg); border: 1px solid var(--sc-bd); border-radius: 12px; padding: 0.625rem 0.75rem 0.625rem 1.125rem; margin-bottom: 1.25rem; cursor: pointer; -webkit-tap-highlight-color: transparent; }
/* RESERVED, NOT HIDDEN (E19, 28 Aug 2026). renderNextUp() is a network read and the card sits above
   the stat tiles, so display:none until it answered meant Home painted, then shoved its own middle
   down by a card. `is-pending` lays the card out and paints nothing: visibility:hidden also takes
   the taps, and renderNextUp() drops the class whichever way it goes — a real card on one side,
   display:none on the other. The height is the real card's rather than a magic number, because the
   two empty lines below give the name and sub-line their true line boxes at their true font sizes;
   `.next-up-go` already carries "Start →" in the markup and measures itself. */
.next-up.is-pending { display: block; visibility: hidden; }
.next-up.is-pending .next-up-name::before,
.next-up.is-pending .next-up-sub::before { content: '\00a0'; }
.next-up::before { content: ''; position: absolute; left: 0; top: 0; bottom: 0; width: 4px; background: var(--sc); }
.next-up:active { filter: brightness(1.35); }
.next-up-main { display: flex; align-items: center; gap: 12px; }
.next-up-text { flex: 1; min-width: 0; }
.next-up-name { font-family: 'Bebas Neue', sans-serif; font-size: 26px; line-height: 1; letter-spacing: 0.5px; color: var(--sc); }
/* Ellipsis rather than wrap: a second line here would put the height straight back. */
.next-up-sub { font-size: 11px; color: var(--muted2); margin-top: 2px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.next-up-go { flex: none; border: 1px solid var(--sc); background: color-mix(in srgb, var(--sc) 12%, transparent); border-radius: 8px; padding: 8px 12px; font-size: 12px; font-weight: 600; color: var(--sc); white-space: nowrap; }
/* One line under the greeting, not a card (19 Aug 2026). As a bordered tile with an accent rail it
   read as something to act on and sat directly above Next up, which is the only thing on Home that
   actually wants tapping. Empty until the quote loads, so a failed fetch leaves nothing rather than
   a stuck "Loading...".
   min-height is 3em against a line-height of 1.5, i.e. exactly TWO lines, and it is deliberately a
   floor and not a fixed height (E19, 28 Aug 2026). Of the 23 quotes, half fit one line on a phone
   and half take two, and the longest takes three — so two lines is the height that costs nothing on
   most days, and a quote that runs over is allowed to run over rather than being clipped. Before
   this the block had no height at all until the fetch answered, and the answer arriving pushed the
   whole page down. A failed fetch now leaves two lines of nothing, which is the price of the page
   not moving: a stable gap beats a jump. */
.daily-quote { font-size: 12.5px; line-height: 1.5; min-height: 3em; color: var(--muted); margin: -0.5rem 0 1.25rem; }
/* Laid out, painting nothing, until renderDailyQuote() drops the class. */
.daily-quote.is-pending { visibility: hidden; }
.quote-text { font-style: italic; }
.quote-author { color: var(--muted2); white-space: nowrap; margin-left: 4px; }
.landing-stats { display: grid; grid-template-columns: repeat(3, 1fr); gap: 8px; margin-bottom: 1.25rem; }
.landing-stats-2 { grid-template-columns: repeat(2, 1fr); margin-bottom: 1.25rem; }
/* No negative top margin any more: the blocks above it now own their own spacing, and pulling the
   label up into them was half of why Home looked stacked on top of itself. */
.landing-section-label { font-size: 11px; color: var(--muted); text-transform: uppercase; letter-spacing: 0.5px; margin: 0 0 0.625rem; }
.landing-stat { background: var(--bg2); border: 1px solid var(--border); border-radius: 10px; padding: 0.75rem; text-align: center; }
.landing-stat-value { font-family: 'Bebas Neue', sans-serif; font-size: 24px; line-height: 1; }
.landing-stat-label { font-size: 10px; color: var(--muted); text-transform: uppercase; letter-spacing: 0.5px; margin-top: 3px; }
.landing-actions { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; margin-bottom: 1.25rem; }
.landing-btn { background: var(--bg2); border: 1px solid var(--border2); border-radius: 12px; padding: 1.25rem 1rem; text-align: center; cursor: pointer; transition: all 0.15s; }
.landing-btn:active { background: var(--bg3); border-color: var(--accent); }
.landing-btn-icon { font-size: 28px; margin-bottom: 6px; }
.landing-btn-label { font-size: 13px; font-weight: 600; color: var(--text); }
.landing-btn-sub { font-size: 11px; color: var(--muted); margin-top: 2px; }
.landing-btn.primary { border-color: var(--accent); background: color-mix(in srgb, var(--accent) 8%, transparent); }

/* STOPWATCH PILL */
/* ─── INLINE WATCH (inside each exercise tile) ─────────── */
/* Header row of the exercise block now has a small watch button
   on the right. Tapping it starts/stops the rest timer for THAT
   exercise. Idle = grey icon, running = orange ring + tiny time. */
.ex-name-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 10px;
  margin-bottom: 4px;
}
/* E2 leftover, 28 Aug 2026. Del on the meta row: the ✕ and the timer button "look clipped against
   the card edge". The block's 0.875rem padding is measured for TEXT, and the same measurement reads
   tighter around a 34px circle than it does beside a letter — a circle touches its bounding box at
   one point and the eye reads that point as the edge. The pair gets its own gap on top of the
   block's, so the controls sit off the edge rather than on it. */
.ex-name-row { padding-right: 5px; }

.ex-watch {
  position: relative;
  width: 34px;
  height: 34px;
  border-radius: 50%;
  background: color-mix(in srgb, var(--text) 8%, transparent);
  border: none;
  padding: 0;
  cursor: pointer;
  flex-shrink: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  transition: transform 0.15s cubic-bezier(0.34, 1.3, 0.64, 1), background 0.25s;
  -webkit-tap-highlight-color: transparent;
}
.ex-watch:active { transform: scale(0.9); }

/* Open Workout: exercise name grows to fill the row, pushing the remove/watch buttons right */
.ex-name-row .ex-name-display { flex: 1; min-width: 0; margin-bottom: 0; }
.ex-remove-btn {
  flex-shrink: 0;
  width: 28px;
  height: 28px;
  border-radius: 50%;
  border: 1px solid var(--border2);
  background: transparent;
  color: var(--muted);
  font-size: 13px;
  line-height: 1;
  cursor: pointer;
  display: flex;
  align-items: center;
  justify-content: center;
  -webkit-tap-highlight-color: transparent;
}
.ex-remove-btn:active { background: color-mix(in srgb, var(--text) 6%, transparent); }

.ex-watch-ring {
  position: absolute;
  inset: 2px;
  width: 30px;
  height: 30px;
  transform: rotate(-90deg);
  pointer-events: none;
}
.ex-watch-bg { fill: none; stroke: color-mix(in srgb, var(--text) 8%, transparent); stroke-width: 2; }
.ex-watch-fill {
  fill: none;
  stroke: var(--accent);
  stroke-width: 2.5;
  stroke-linecap: round;
  stroke-dasharray: 75.4;
  stroke-dashoffset: 75.4;
  opacity: 0;                 /* hidden when idle */
  transition: stroke-dashoffset 0.95s linear, stroke 0.3s, opacity 0.2s;
}

.ex-watch-inner {
  position: relative;
  display: flex;
  align-items: center;
  justify-content: center;
  color: var(--muted2);
  transition: color 0.2s;
}
.ex-watch-icon { width: 15px; height: 15px; }

/* RUNNING state */
.ex-watch.running .ex-watch-fill { opacity: 1; stroke: var(--red); }
.ex-watch.running .ex-watch-inner { color: var(--red); }
.ex-watch.running .ex-watch-icon { display: none; }
.ex-watch.running .ex-watch-time { display: block; }

/* DONE state (target rest reached) */
.ex-watch.done .ex-watch-fill { stroke: var(--green); }
.ex-watch.done .ex-watch-inner { color: var(--green); }

/* Flash green when a rest is saved */
.ex-watch.flash-green { background: color-mix(in srgb, var(--green) 85%, transparent); }
.ex-watch.flash-green .ex-watch-inner { color: #fff; }

.ex-watch-time {
  display: none;
  font-family: 'DM Mono', monospace;
  font-size: 9px;
  letter-spacing: -0.3px;       /* tighter so "0:22" stays inside the ring */
  font-variant-numeric: tabular-nums;
  line-height: 1;
  font-weight: 500;
}

/* ─── REST LINE (under completed set rows) ─────────────── */
.rest-line {
  font-family: 'DM Mono', monospace;
  font-size: 10px;
  color: var(--green);
  padding: 2px 0 6px 24px;    /* indent under the set number column */
  min-height: 0;
  line-height: 1;
}
.rest-line:empty { display: none; }

/* ─── HISTORY PAGE ─────────────────────────────────────── */
.history-filters { margin-bottom: 1.25rem; }

.history-tabs { display: flex; gap: 6px; margin-bottom: 10px; }
.history-tab {
  flex: 1;
  padding: 9px 10px;
  background: var(--bg3);
  border: 1px solid var(--border2);
  border-radius: 8px;
  color: var(--muted);
  font-size: 12px;
  font-weight: 600;
  font-family: 'DM Sans', sans-serif;
  cursor: pointer;
  letter-spacing: 0.5px;
  transition: all 0.15s;
}
.history-tab.active {
  border-color: var(--accent);
  background: color-mix(in srgb, var(--accent) 12%, transparent);
  color: var(--accent);
}

.history-selects { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; margin-bottom: 10px; }
.history-select {
  width: 100%;
  background: var(--bg3);
  border: 1px solid var(--border2);
  border-radius: 8px;
  padding: 10px 13px;
  color: var(--text);
  font-size: 13px;
  font-family: 'DM Sans', sans-serif;
  outline: none;
  cursor: pointer;
  -webkit-appearance: none;
  appearance: none;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='8' viewBox='0 0 12 8' fill='none' stroke='%23888' stroke-width='2' stroke-linecap='round'%3E%3Cpolyline points='2,2 6,6 10,2'/%3E%3C/svg%3E");
  background-repeat: no-repeat;
  background-position: right 12px center;
  padding-right: 32px;
  transition: border-color 0.2s;
}
.history-select:focus { border-color: var(--accent); }
.history-select.has-value { border-color: var(--accent); color: var(--accent); }

.history-search {
  width: 100%;
  background: var(--bg3);
  border: 1px solid var(--border2);
  border-radius: 8px;
  padding: 10px 13px;
  color: var(--text);
  font-size: 13px;
  font-family: 'DM Sans', sans-serif;
  outline: none;
  transition: border-color 0.2s;
}
.history-search:focus { border-color: var(--accent); }

/* ─── FIND IN HISTORY (26 Aug 2026) ────────────────────────
   Del asked for Ctrl+F, so the hits are marked where they sit and one of them is the active one.

   --accent, tinted, is the "this is the thing you searched for" wash. It does not fight
   accent-as-tappable: this only ever paints behind text inside a card, never on or near a control,
   and the one control it does colour (the find bar's arrows) is the control FOR the search. The
   active hit inverts to solid accent so the row the page has just scrolled to is unmistakable at
   arm's length, which is the whole point of the feature. */
mark.hl {
  background: color-mix(in srgb, var(--accent) 22%, transparent);
  color: inherit;
  border-radius: 3px;
  padding: 0 2px;
}
mark.hl.on { background: var(--accent); color: #fff; }

/* Same box, same slot, same shadow as .update-banner. The app already has exactly one floating bar
   and this is deliberately that object again rather than a second idea about what a bar looks like.
   Fixed rather than in the filter row because the feed scrolls a long way to reach a hit and the
   arrows have to still be under a thumb when it gets there.

   z-index 90: under the bottom nav's 100 (it sits above it, they never overlap) and well under the
   200/300 modals, so opening a card's edit sheet covers it. */
.find-bar {
  position: fixed;
  left: 12px; right: 12px; bottom: 78px;
  z-index: 90;
  max-width: 520px;
  margin: 0 auto;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 10px;
  padding: 7px 8px 7px 14px;
  background: var(--bg3);
  border: 1px solid var(--accent);
  border-radius: 10px;
  box-shadow: 0 6px 20px rgba(35,31,26,0.16);
}
/* "3 of 12+ matches" — a counter, so --muted2 rather than the accent: it is a readout, not
   something to tap. tabular-nums stops the bar twitching as the number changes while typing. */
.find-count {
  font-size: 12px;
  font-weight: 600;
  color: var(--muted2);
  letter-spacing: 0.02em;
  font-variant-numeric: tabular-nums;
}
.find-nav { display: flex; gap: 6px; }
.find-btn {
  min-width: 40px;
  padding: 7px 10px;
  font-size: 14px;
  font-weight: 700;
  line-height: 1;
  color: var(--accent);
  background: color-mix(in srgb, var(--accent) 10%, transparent);
  border: 1px solid var(--border2);
  border-radius: 7px;
  cursor: pointer;
  font-family: 'DM Sans', sans-serif;
}
.find-btn:active { background: color-mix(in srgb, var(--accent) 26%, transparent); }
/* Clear is the way out, not the action — it recedes. */
.find-close { color: var(--muted); background: transparent; border-color: transparent; }

.history-date-group { margin-bottom: 1.5rem; }
.history-date-header {
  font-size: 11px;
  color: var(--muted);
  text-transform: uppercase;
  letter-spacing: 0.5px;
  margin-bottom: 8px;
  font-weight: 600;
}

.history-card {
  background: var(--bg2);
  border: 1px solid var(--border);
  border-radius: 10px;
  padding: 0.875rem;
  margin-bottom: 8px;
  cursor: pointer;
  transition: border-color 0.15s;
}
.history-card:active { border-color: var(--accent); }
.history-card-notes {
  font-size: 12px;
  color: var(--muted);
  margin-top: 6px;
  line-height: 1.4;
}

/* ─── SESSION TEMPLATE EDITOR ─────────────────────────────── */
/* flex-wrap so the ⇄ superset button and its picker drop onto their own full-width lines below the
   name/controls row rather than fighting them for space (11 Aug 2026) */
.template-ex-row { display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: 8px; background: var(--bg3); border-radius: 8px; padding: 8px 10px; margin-bottom: 6px; }
.template-ex-row .ss-btn, .template-ex-row .ss-picker { flex: 0 0 100%; margin-top: 0; }
.template-ex-row.in-superset { border-left: 2px solid var(--blue); }
.template-ex-name { font-size: 13px; font-weight: 600; color: var(--text); flex: 1; min-width: 0; }
.template-ex-controls { display: flex; align-items: center; gap: 4px; flex-shrink: 0; }
.template-ex-btn { width: 26px; height: 26px; padding: 0; display: flex; align-items: center; justify-content: center; font-size: 13px; border-radius: 6px; }
.template-ex-btn:disabled { opacity: 0.3; cursor: default; }
.template-ex-sets { font-size: 11px; color: var(--muted2); font-family: 'DM Mono', monospace; width: 48px; text-align: center; }

.edit-template-link { display: block; text-align: center; font-size: 12px; font-weight: 600; color: var(--accent); background: color-mix(in srgb, var(--accent) 8%, transparent); border: 1px solid var(--accent); border-radius: 8px; padding: 9px; margin-bottom: 0.75rem; cursor: pointer; }
.edit-template-link:active { background: color-mix(in srgb, var(--accent) 16%, transparent); }

/* ─── STATS: HERO + MACROS (redesign 10 Aug 2026) ─────────── */
.stats-hero { text-align: center; padding: 0.25rem 0 0; }
.stats-hero-val { font-family: 'Bebas Neue', sans-serif; font-size: 54px; line-height: 1; letter-spacing: 1px; color: var(--accent); }
.stats-hero-unit { font-size: 15px; color: var(--muted); margin-left: 3px; font-family: 'DM Sans', sans-serif; letter-spacing: 0; }
.stats-hero-sub { font-size: 12px; margin-top: 4px; font-weight: 600; }
.stats-hero-sub.down { color: var(--green); }
.stats-hero-sub.up { color: var(--red); }
.stats-hero-sub.flat { color: var(--muted); }
/* ─── WEEKLY AVERAGE WEIGHT (17 Aug 2026) ─────────────────
   One card, one job: pick a week, see that week's average, see it against the week you're in.
   No sessions, no calories, no week-on-week deltas — every one of those was tried in a mockup and
   they all buried the number the card exists for. */
/* A stepper — one week at a time, no list of any kind. Two earlier attempts were rejected: a native
   <select> (OS dropdown chrome, full window width on desktop) and a scrolling pill row (there are
   weigh-ins in the database from April, so five irrelevant pills sat in front of the useful ones).
   A stepper has neither problem: nothing is on screen except the week you're looking at, and April
   is seventeen deliberate taps away rather than in your face.
   Everything lives in a fixed-width column that is centred explicitly — the card is full-window
   width on a desktop browser, and relying on text-align alone left the number visibly off-centre. */
.weekavg-body { max-width: 260px; margin: 0 auto; text-align: center; }
.weekavg-nav, .stats-range-nav { display: flex; align-items: center; justify-content: space-between; gap: 12px; }
.nav-arrow {
  flex: 0 0 auto; width: 38px; height: 38px; border-radius: 50%;
  border: 1.5px solid var(--muted); background: var(--bg); color: var(--text);
  font-size: 22px; font-weight: 700; line-height: 1; font-family: 'DM Sans', sans-serif; cursor: pointer;
  display: flex; align-items: center; justify-content: center; transition: all 0.15s;
}
.nav-arrow:active { border-color: var(--accent); color: var(--accent); }
.nav-arrow:disabled { opacity: 0.35; cursor: default; }
.weekavg-wk, .stats-range-mid { flex: 1 1 auto; min-width: 0; }
.weekavg-wk-name, .stats-range-name { font-family: 'Bebas Neue', sans-serif; font-size: 23px; letter-spacing: 0.5px; line-height: 1; color: var(--text); }
.weekavg-range, .stats-range-span { font-size: 10px; color: var(--muted); text-transform: uppercase; letter-spacing: 0.9px; margin-top: 4px; white-space: nowrap; }
.weekavg-val { font-family: 'Bebas Neue', sans-serif; font-size: 56px; line-height: 1; letter-spacing: 1px; color: var(--accent); margin-top: 1rem; }
.weekavg-unit { font-family: 'DM Sans', sans-serif; font-size: 15px; color: var(--muted); margin-left: 3px; letter-spacing: 0; }
.weekavg-cmp { font-size: 12.5px; font-weight: 600; margin-top: 5px; min-height: 18px; }
.weekavg-cmp.down { color: var(--green); }
.weekavg-cmp.up   { color: var(--red); }
.weekavg-cmp.flat { color: var(--muted); }
/* Waist, sessions, calories and steps as a 2×2 under the weight (23 Aug 2026 — Del picked layout
   07 off a proof sheet of fourteen). What this replaced was a full-width waist band plus a
   three-column split: five stacked bands, and a third column with 99px to fit "12,344" over
   "AVG STEPS" over "MON–SAT · 6 DAYS", all three of which wrapped. Four cells across the card's
   full width give every figure ~148px, and the card loses a band of height.

   The hairlines are the container's own background showing through a 1px grid gap, not borders on
   the cells. That draws the full cross for a 2×2 and two clean uprights for a 3-across without a
   single nth-child rule — which matters, because the waist cell drops out entirely for anyone who
   has never measured one and the remaining three reflow to one row. See .cols3 and
   showWeeklyWaist(). Cells stretch to their row's height, so the hairline stays straight even
   though only three of the four carry a third line. */
.weekavg-quad-wrap { margin-top: 0.9rem; padding-top: 0.8rem; border-top: 1px solid var(--border2); }
.weekavg-quad { display: grid; grid-template-columns: 1fr 1fr; gap: 1px; background: var(--border2); }
.weekavg-quad.cols3 { grid-template-columns: repeat(3, 1fr); }
.weekavg-cell { background: var(--bg2); text-align: center; padding: 10px 4px 11px; }
.weekavg-cell-val { font-family: 'Bebas Neue', sans-serif; font-size: 26px; line-height: 1; letter-spacing: 0.5px; font-variant-numeric: tabular-nums; }
.weekavg-cell-name { font-size: 9.5px; text-transform: uppercase; letter-spacing: 1px; color: var(--muted); margin-top: 3px; }
/* The optional third line: a day span under an average, a comparison under the waist. Keep the
   strings short — at this width and size, anything past about twenty characters wraps. */
.weekavg-cell-note { font-size: 8.5px; text-transform: uppercase; letter-spacing: 0.8px; color: var(--border2); margin-top: 3px; }
.weekavg-cell-note.down { color: var(--green); }
.weekavg-cell-note.up   { color: var(--red); }
.weekavg-cell-note.flat { color: var(--muted); }
/* The shared day-span note (23 Aug 2026). Under the whole grid rather than under two of the cells,
   so the labels keep a common baseline and the words fit on one line. Empty on a full week and
   whenever calories and steps disagree — see showWeeklySplit(). */
.weekavg-split-note { font-size: 9px; text-transform: uppercase; letter-spacing: 0.8px; color: var(--border2); text-align: center; margin-top: 0.55rem; }
.weekavg-split-note:empty { display: none; }
/* The week's running step total (28 Sept 2026) — a reference line, so muted rather than faint. */
.weekavg-steps-total { font-size: 10px; text-transform: uppercase; letter-spacing: 0.8px; color: var(--muted); text-align: center; margin-top: 0.55rem; font-variant-numeric: tabular-nums; }
.weekavg-steps-total:empty { display: none; }

/* The chart's range stepper (23 Aug 2026). It shares every rule below with the week card's own
   arrows — same 38px circles, same two-line centre — because they are the same control doing the
   same job on the same page, and the pills that were here first read as another app's furniture.
   Narrower than the week card's 260px body: it sits over a full-width chart. */
/* The weight tile (23 Aug 2026), which carries three faces since 27 Aug: daily, weekly, food.

   The perspective lives on the outer box and the rotation on the inner one, which is the only
   arrangement where the faces share a vanishing point — put the perspective on the rotating element
   itself and the back face swings through a different arc from the front one, which reads as two
   cards rather than two sides of one.

   ⚠️ IT SPINS, AND IT SPINS WITH THREE FACES (27 Aug 2026 — Del: "i want the spin back"). A coin
   has two sides, so the trick is that only ever TWO faces are mounted: the inner keeps turning
   180° per step and accumulating (0 → 180 → 360 → …), and flipStats() re-parks every face's own
   rotateY on each step so that the incoming one is the front and the rest are on the back, where
   backface-visibility hides them. That is why a face's transform is set in JS and not here — the
   angle it needs depends on where the tile has spun to, not on which face it is.
   Height is still set in px by sizeStatsFlip(); see the note in index.html for why. */
/* ⚠️ THE HEIGHT LIVES ON THE TILE, THE ROTATION ON THE INNER, AND THEY MUST STAY APART. The snap
   at the end of a turn kills the inner's transition for one frame; with the height on the inner
   too, that would snap the height as well and the tile would jump. */
.flip-tile { perspective: 1400px; transition: height 0.6s cubic-bezier(0.2, 0.7, 0.25, 1); }
/* Height is animated alongside the turn (27 Aug 2026). The tile used to stand at the height of its
   TALLEST face, which was fine while both faces were charts and dreadful the moment a shorter one
   joined them: the food card ended up a small card marooned in a tile sized for the daily chart,
   with a hand's width of nothing between it and the switch. It takes the height of the face that is
   actually showing now, and eases into it on the same curve as the spin. */
.flip-inner { position: relative; width: 100%; height: 100%; transform-style: preserve-3d; transition: transform 0.6s cubic-bezier(0.2, 0.7, 0.25, 1); }
/* top/left/width rather than inset:0 — a face stretched to all four edges takes its height FROM the
   wrapper, and the wrapper takes its height from the faces, so sizeStatsFlip() would be measuring
   its own output and the tile could never shrink again. Auto height keeps the measurement honest. */
.flip-face { position: absolute; top: 0; left: 0; width: 100%; backface-visibility: hidden; -webkit-backface-visibility: hidden; }
/* Pointer events follow the visible face, not the z-order. Without this a hidden face's arrows
   still take taps through the front one — invisible buttons that work are worse than visible ones
   that don't. ⚠️ Never add `visibility: hidden` here: the outgoing face has to stay paintable for
   the length of the turn or it vanishes instead of rotating away. backface-visibility is what hides
   it, and only once it is actually facing away. */
.flip-face { pointer-events: none; }
.flip-face.active { pointer-events: auto; }
/* The tile's own card, so side A reads as the same object as the week card on side B rather than as
   the page background with a chart drawn on it. */
.stats-weight-card { padding: 0.875rem; }

/* The switch under the tile. A track with a thumb that slides, rather than two buttons that light
   up: the thumb moving left-to-right is the same motion the tile makes, so the control looks like
   what it does. Sized to the tile's own text column, not the full width — it is a caption on the
   card above it, not an action bar. */
.flip-switch {
  position: relative;
  display: flex;
  max-width: 260px;
  margin: 0.75rem auto 0.875rem;
  padding: 3px;
  background: var(--bg3);
  border: 1px solid var(--border);
  border-radius: 999px;
}
/* The thumb's width and position are set inline by renderStatsFlipSwitch() / flipStats(), because
   the number of options is not fixed: an account with no full week yet is offered two, not three.
   A hard-coded 50% here would size the thumb for a switch that may not have two options on it. */
.flip-switch-thumb {
  position: absolute;
  top: 3px; left: 3px;
  width: calc(50% - 3px);
  height: calc(100% - 6px);
  border-radius: 999px;
  background: color-mix(in srgb, var(--accent) 13%, transparent);
  border: 1px solid var(--accent);
  transition: transform 0.35s cubic-bezier(0.2, 0.7, 0.25, 1);
}
.flip-switch-opt {
  position: relative;          /* over the thumb, so the label never sits behind it */
  flex: 1;
  padding: 7px 0;
  background: none;
  border: none;
  border-radius: 999px;
  font-family: 'DM Sans', sans-serif;
  font-size: 11.5px;
  font-weight: 600;
  letter-spacing: 0.6px;
  text-transform: uppercase;
  color: var(--muted);
  cursor: pointer;
  transition: color 0.2s;
}
.flip-switch-opt.active { color: var(--accent); }

@media (prefers-reduced-motion: reduce) {
  .flip-inner, .flip-switch-thumb { transition: none; }
}

.stats-range-nav { max-width: 230px; margin: 0.75rem auto 0; text-align: center; }

.stats-range-nav .nav-arrow { width: 32px; height: 32px; font-size: 19px; }
.stats-range-name { font-size: 20px; }

.stats-chart { position: relative; margin-top: 0.5rem; margin-bottom: 0.875rem; }
.stats-chart svg { width: 100%; height: auto; display: block; touch-action: pan-y; }

/* The scrub readout. `pan-y` above rather than `none`: a drag across the chart reads it, but a
   swipe down it still scrolls the page — the chart is full-width, so making it a scroll dead zone
   would cost more than the readout is worth. Positioned in px by bindChartScrub() against the
   rendered box, and pointer-events off so it can never eat the drag that is moving it. */
.chart-tip {
  position: absolute;
  display: flex;
  align-items: baseline;
  gap: 6px;
  padding: 4px 8px;
  background: var(--bg2);
  border: 1px solid var(--border2);
  border-radius: 7px;
  box-shadow: 0 2px 8px rgba(35, 31, 26, 0.13);
  white-space: nowrap;
  pointer-events: none;
  font-variant-numeric: tabular-nums;
  z-index: 2;
}
.chart-tip[hidden] { display: none; }
.chart-tip-date { font-size: 10px; color: var(--muted); }
.chart-tip-val { font-family: 'DM Mono', monospace; font-size: 12px; font-weight: 500; color: var(--text); }

/* Stats → the FOOD face of the tile (E6, 27 Aug 2026). Was a standalone `.macro-card` under the
   tile with a calorie hero and three meter rows — Del: "stands out like a fucking ugly toe", and it
   was, because it was the only box on the page that was not the tile.
   It is a face of the tile now and it deliberately wears the WEEKLY face's furniture: the same
   .stats-hero figure, the same .weekavg-quad cells, the same title row. The three meter rows went
   with the card — three tracks that were nearly always full said less than three numbers do, and
   the cells line up with the weekly face behind them. Tabular so the figures sit on a common grid. */
.stats-food-card { padding: 0.875rem; font-variant-numeric: tabular-nums; }
/* The same arrows as the other two faces (27 Aug 2026 — Del: "it needs arrows to cycle through on
   that page"). It is `.stats-range-nav`'s own control, re-parked at the top of the card where the
   weekly face keeps its week arrows, so all three faces are steered from the same place. */
.food-nav { margin: 0 auto; }
/* The figure carries the same weight as the weekly face's — same size, same unit, same comparison
   line under it. Three faces of one tile, so the headline number sits at one size on all three. */
.stats-food-card .stats-hero { padding: 0; margin-top: 1rem; }
.stats-food-card .stats-hero-val { font-size: 56px; }
.stats-food-card .stats-hero-unit { font-size: 15px; }
.stats-food-card .stats-hero-sub { font-size: 12.5px; margin-top: 5px; min-height: 18px; }
/* The signed delta is the only coloured thing on this line — red over target, green on or under —
   so it needs its own gap where the interpunct used to be, not a bullet doing the spacing. */
.stats-food-card .macro-cal-delta { margin-left: 7px; }
/* The grams, in DM Sans beside the figure rather than inside it — Bebas Neue has no lowercase, so a
   "g" set in the figure's own face renders as "G". Same split kcal and kg already use. */
.stats-food-card .cell-unit { font-family: 'DM Sans', sans-serif; font-size: 12px; font-weight: 400; color: var(--muted); letter-spacing: 0; margin-left: 1px; }
/* And the target under it is not shouted, so its own g stays a g. */
.stats-food-card .weekavg-cell-note { text-transform: none; font-size: 10px; letter-spacing: 0.2px; }

/* ─── HISTORY: PROGRESS FEED CARDS (redesign 10 Aug 2026) ─── */
/* Left rail colour keys the entry type: amber = workout, blue = daily check-in. */
.pf-card { background: var(--bg2); border: 1px solid var(--border); border-radius: 14px; padding: 0.875rem; margin-bottom: 10px; position: relative; overflow: hidden; cursor: pointer; transition: border-color 0.15s; }
.pf-card::before { content: ''; position: absolute; left: 0; top: 0; bottom: 0; width: 3px; background: var(--amber); }
.pf-card.log::before { background: var(--blue); }
.pf-card:active { border-color: var(--accent); }
.pf-head { display: flex; justify-content: space-between; align-items: baseline; gap: 10px; margin-bottom: 9px; }
.pf-name { font-family: 'Bebas Neue', sans-serif; font-size: 22px; letter-spacing: 0.5px; color: var(--amber); }
.pf-card.log .pf-name { color: var(--blue); }
.pf-date { font-size: 10.5px; color: var(--muted); flex-shrink: 0; }
.pf-lift { display: grid; grid-template-columns: 1fr auto 46px; gap: 8px; align-items: center; padding: 6px 0; }
.pf-lname { font-size: 12.5px; color: var(--text); }
.pf-sub { font-size: 10px; color: var(--muted); margin-top: 2px; font-family: 'DM Mono', monospace; }
.pf-lval { font-family: 'DM Mono', monospace; font-size: 12.5px; font-weight: 500; color: var(--text); }
.pf-d { font-family: 'DM Mono', monospace; font-size: 10.5px; font-weight: 500; text-align: right; }
.pf-d.up { color: var(--green); }
.pf-d.down { color: var(--red); }
.pf-d.same { color: var(--muted); }
.pf-d.neutral { color: var(--text); opacity: 0.65; }
/* One state, not three (19 Aug 2026). This started as green/red/amber verdicts on every macro row
   and went through a percent delta and a printed ±band before the actual fault showed: a card that
   grades five rows hands you five colours to decode, and the green ones were decoration — they said
   "carry on", which is what a blank row says for free. So the only macro colour left is this one,
   and it appears on a row that is genuinely off. Red since 20 Aug, not amber: missCell() only ever
   prints this cell for goalState() === 'bad', so it says the same thing .pf-d.down two lines up
   says — this went the wrong way — and the two now say it in the same colour. See missCell(). */
.pf-d.off { color: var(--red); }

/* Macro target meters on the Check-in card */
.goal-block { margin-bottom: 1rem; }
.goal-block-head { display: flex; justify-content: space-between; align-items: center; font-size: 10px; color: var(--muted); text-transform: uppercase; letter-spacing: 0.5px; margin-bottom: 8px; }
.goal-edit-link { background: none; border: none; color: var(--accent); font-size: 10px; font-family: 'DM Sans', sans-serif; font-weight: 600; text-transform: uppercase; letter-spacing: 0.5px; cursor: pointer; padding: 0; }
.goal-empty { font-size: 12px; color: var(--muted); padding: 4px 0; }
.goal-row { display: grid; grid-template-columns: 50px 78px 1fr 52px; align-items: center; gap: 6px; padding: 5px 0; }
.goal-name { font-size: 10px; color: var(--muted); text-transform: uppercase; letter-spacing: 0.3px; }
.goal-val { font-family: 'DM Mono', monospace; font-size: 10.5px; color: var(--muted); white-space: nowrap; }
.goal-val b { font-weight: 500; }
.goal-track { height: 5px; border-radius: 3px; background: color-mix(in srgb, var(--text) 12%, transparent); overflow: hidden; }
.goal-fill { display: block; height: 100%; border-radius: 3px; transition: width 0.3s ease; }
.goal-left { font-family: 'DM Mono', monospace; font-size: 9px; color: var(--muted); text-align: right; white-space: nowrap; }

/* Verdict colours. Text (.gv-*) and meter fill (.goal-fill.*) are deliberately separate classes —
   one class setting both `color` and `background` paints a coloured block behind every number it
   lands on.
   There used to be `.macro-cal-line b.gv-*` overrides here, because that rule set `color` on its
   own `b` and so outranked a bare `.gv-*`. Its replacement `.macro-cal-target b` sets only weight
   and spacing, never colour, so the verdict class applies directly and the overrides are gone.
   Keep it that way: a `b` inside a verdict-coloured element must not declare its own colour. */
.gv-good  { color: var(--green); }
.gv-bad   { color: var(--red); }
/* 'soft' is under budget on carbs / fat / calories — deliberately NOT a miss (goalState()), so it
   must not borrow the attention colour and claim to be one. It takes no colour at all: the row is
   logged, and there is nothing to say about it. */
.gv-soft  { color: var(--muted2); }
.gv-empty { color: var(--muted); }
.goal-fill.good  { background: var(--green); }
.goal-fill.bad   { background: var(--red); }
.goal-fill.soft  { background: var(--muted); }
.goal-fill.empty { background: var(--border2); }

.goal-hint { font-size: 10px; color: var(--muted); margin-top: 5px; line-height: 1.4; }
/* Check-in metrics (19 Aug 2026). Every row used to be its own .pf-lift with an `auto` value
   column, so "79.9kg", "1985 / 2000" and "171 / 175g" each set their own left edge and the slashes
   staggered down the card. One grid for the whole block gives the number, the slash and the target
   a shared column each, and the deltas a shared right edge. Columns stay content-sized so nothing
   can overflow into its neighbour whatever the figures turn out to be. */
.pf-metrics { display: grid; grid-template-columns: 1fr auto auto auto minmax(46px, auto); column-gap: 8px; row-gap: 11px; align-items: baseline; }
.pf-mname { font-size: 12.5px; color: var(--text); }
.pf-mval { font-family: 'DM Mono', monospace; font-size: 12.5px; font-weight: 500; color: var(--text); text-align: right; }
/* Weight and waist have no target to print, so they span the three number columns. */
.pf-mwide { grid-column: span 3; }
.pf-msep { font-family: 'DM Mono', monospace; font-size: 12.5px; color: var(--muted2); }
.pf-mtgt { font-family: 'DM Mono', monospace; font-size: 12.5px; color: var(--muted); }
.pf-metrics .pf-sub { display: block; }
.pf-card.log .history-card-notes { margin-top: 11px; }
.pf-badge { display: inline-block; font-size: 9px; font-weight: 700; letter-spacing: 0.08em; text-transform: uppercase; background: var(--green); color: var(--bg2); border-radius: 20px; padding: 2px 7px; margin-left: 6px; }
.pf-foot { display: flex; gap: 14px; flex-wrap: wrap; margin-top: 9px; padding-top: 8px; border-top: 1px solid var(--border); font-size: 10.5px; color: var(--muted); }
.pf-foot b { font-family: 'DM Mono', monospace; color: var(--text); font-weight: 500; }
.pf-delete { font-size: 10px; color: var(--red); letter-spacing: 0.5px; cursor: pointer; font-weight: 600; margin-left: auto; }

/* ─── ENERGY SLIDER (replaces emoji picker, 10 Aug 2026) ──── */
/* value 0 = not set; 1–5 map to Flat/Low/OK/Good/Strong. */
.energy-range { -webkit-appearance: none; appearance: none; width: 100%; height: 8px; border-radius: 5px; background: linear-gradient(90deg, #ded4c2, var(--accent)); outline: none; padding: 0; margin: 4px 0 0; }
.energy-range::-webkit-slider-thumb { -webkit-appearance: none; appearance: none; width: 20px; height: 20px; border-radius: 50%; background: var(--accent); border: 3px solid var(--bg2); box-shadow: 0 1px 4px rgba(35,31,26,0.20); cursor: pointer; }
.energy-range::-moz-range-thumb { width: 20px; height: 20px; border-radius: 50%; background: var(--accent); border: 3px solid var(--bg2); box-shadow: 0 1px 4px rgba(35,31,26,0.20); cursor: pointer; }
.energy-labels { display: flex; justify-content: space-between; align-items: baseline; font-size: 9.5px; color: var(--muted); margin-top: 8px; }
.energy-labels b { color: var(--accent); font-size: 11px; font-weight: 600; }
/* The thumb has to rest somewhere, and it rests on Flat. While that is only where the control sits
   rather than an answer Del gave, the word reads muted — the rail itself never says "not set". */
.energy-labels b.energy-unset { color: var(--muted); font-weight: 500; }

/* Quick-fill row at the top of the check-in modal */
.quick-row { display: flex; gap: 6px; margin-bottom: 0.875rem; }
.quick-btn { flex: 1; background: color-mix(in srgb, var(--accent) 8%, transparent); border: 1px solid var(--accent); border-radius: 8px; padding: 9px 6px; font-size: 11px; font-weight: 600; color: var(--accent); font-family: 'DM Sans', sans-serif; cursor: pointer; }
.quick-btn:active { background: color-mix(in srgb, var(--accent) 18%, transparent); }
.quick-btn.ghost { background: var(--bg3); border-color: var(--border2); color: var(--muted2); }

/* ─── LAST TIME CARD ──────────────────────────────────────── */
.last-time-card { padding: 0.75rem 0.875rem; }
.last-time-header { display: flex; align-items: center; justify-content: space-between; cursor: pointer; font-size: 13px; font-weight: 600; color: var(--muted2); }
.last-time-chevron { transition: transform 0.15s; color: var(--muted); }
.last-time-card.expanded .last-time-chevron { transform: rotate(180deg); }
.last-time-body { display: none; margin-top: 0.75rem; }
.last-time-card.expanded .last-time-body { display: block; }
.last-time-row { display: flex; justify-content: space-between; gap: 10px; font-size: 12px; padding: 5px 0; border-top: 1px solid var(--border); }
.last-time-row:first-child { border-top: none; }
.last-time-ex { color: var(--text); }
.last-time-var { color: var(--muted); font-weight: 400; }
.last-time-sets { color: var(--muted2); font-family: 'DM Mono', monospace; text-align: right; flex-shrink: 0; }
/* Rest sits under the sets, dimmer and smaller — it's the second question you ask, never the first */
.last-time-rest { color: var(--muted); font-size: 10px; margin-top: 1px; }
/* Cardio rows in the "Last time" card sit under the lifts, dimmed to read as a different kind of work */
.last-time-cardio .last-time-ex { color: var(--muted2); }
/* The repeat button, Open Workout only (23 Aug 2026). Outline rather than accent: it is an offer,
   and the accent on this screen belongs to Save Workout at the bottom. */
.last-time-load { margin-top: 0.75rem; }

/* ─── SUPERSETS (11 Aug 2026) ─────────────────────────────── */
/* The ⇄ button at the foot of each exercise block opens a picker of what to superset it with. */
.ss-btn { display: block; width: 100%; margin-top: 6px; padding: 7px; font-size: 11px; font-weight: 600; letter-spacing: 0.03em; color: var(--muted); background: none; border: 1px dashed var(--border2); border-radius: 8px; cursor: pointer; font-family: 'DM Sans', sans-serif; }
/* Move-an-exercise-today, 30 Aug 2026. The arrows share the tail row with ⇄ Superset rather than
   going up into .ex-name-row, which already carries the name, the ✕ and the timer ring and is the
   row whose controls were clipping against the card edge on 28 Aug.
   Same dashed, muted treatment as the button beside them: this is a nudge, not a verdict, and it
   takes no colour. A disabled arrow is dimmed rather than hidden, so the row never changes width
   under your thumb as a block reaches the top or the bottom. */
.ex-tail-row { display: flex; align-items: stretch; gap: 6px; }
/* overflow + ellipsis (28 Sept 2026): "⇄ Superset with Single Leg Curl" ran straight over the
   ↘ Drop set button beside it. A long label now ends in … inside its own half of the row. */
.ex-tail-row .ss-btn { flex: 1; min-width: 0; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
/* ↘ Drop set takes half of this row rather than a row of its own, so it costs the tile no height. */
.ss-btn.drop-btn:active { border-style: solid; border-color: var(--accent); color: var(--accent); }
.ex-move-btn { flex: 0 0 38px; margin-top: 6px; padding: 7px 0; font-size: 13px; line-height: 1; color: var(--muted); background: none; border: 1px dashed var(--border2); border-radius: 8px; cursor: pointer; font-family: 'DM Sans', sans-serif; }
.ex-move-btn:active:not(:disabled) { border-style: solid; border-color: var(--accent); color: var(--accent); }
.ex-move-btn:disabled { opacity: 0.3; cursor: default; }
.ss-btn.active { color: var(--blue); border-style: solid; border-color: var(--blue); background: color-mix(in srgb, var(--blue) 8%, transparent); }
/* E18, 30 Aug 2026. The variations button sits directly under the superset one and shares its shape,
   so it must NOT share its colour: --blue has meant superset app-wide since 11 Aug, and two blue
   dashed buttons on one row would say the same thing twice about two unrelated controls.
   Filled state is NEUTRAL — this is identity ("Old Mach / New Mach"), not a verdict and not a
   pairing. Same reasoning as the E2 meta row's neutral track. */
.ss-btn.var-btn-row.active { color: var(--text); border-style: solid; border-color: var(--border2); background: var(--bg2); font-weight: 600; }
/* Linked blocks share a blue rail, so a superset reads as one unit once they've snapped together */
/* The blue edge is drawn INSIDE the block's own width, so the 2px has to come off the padding it
   sits on — without this every child of a paired block walks 2px right and the two round controls
   at the end of the header lose a seventh of the gap they have, on exactly the blocks that are
   already carrying an extra mark. Same 28 Aug fix as .ex-name-row above. */
.exercise-block.in-superset { border-left: 2px solid var(--blue); padding-left: calc(0.875rem - 1px); }

/* Picker — inline under the ⇄ button, one open at a time. Rows are full-width tap targets: this gets
   used mid-set with a phone in one hand, so nothing here is smaller than a thumb. */
.ss-picker { margin-top: 6px; padding: 8px; background: var(--bg3); border: 1px solid var(--border2); border-radius: 8px; }
.ss-picker-title { font-size: 10px; font-weight: 700; letter-spacing: 0.08em; text-transform: uppercase; color: var(--muted2); margin-bottom: 7px; }
.ss-pick { display: block; width: 100%; margin-bottom: 5px; padding: 10px; font-size: 13px; font-weight: 600; text-align: left; color: var(--text); background: var(--bg2); border: 1px solid var(--border); border-radius: 7px; cursor: pointer; font-family: 'DM Sans', sans-serif; }
.ss-pick:active { border-color: var(--blue); }
.ss-pick-note { display: block; margin-top: 2px; font-size: 10px; font-weight: 500; color: var(--muted2); }
.ss-pick-clear { color: var(--red); border-color: color-mix(in srgb, var(--red) 35%, transparent); margin-top: 3px; margin-bottom: 0; }
.ss-pick-add { margin-top: 3px; font-size: 13px; }
.ss-picker-empty { font-size: 12px; color: var(--muted); padding: 2px 0 6px; }
.pf-ss { display: inline-block; font-size: 9px; font-weight: 700; letter-spacing: 0.08em; text-transform: uppercase; background: color-mix(in srgb, var(--blue) 16%, transparent); color: var(--blue); border-radius: 20px; padding: 2px 7px; margin-left: 6px; }

/* ─── BUILD STAMP + UPDATE BANNER (11 Aug 2026) ───────────── */
/* The build the phone is actually running, at the bottom of Home. Deliberately tiny and grey — it's
   diagnostic, not decoration — but tappable to force an update check on the spot. */
/* Sits just above the build stamp at the foot of Home — deliberately quiet, it's used twice a year. */
/* Two links since 13 Aug 2026 (Export / Change password). Flex rather than centred text so they
   stay evenly spaced, and wrap onto two lines on a narrow phone instead of running together. */
.account-row { display: flex; justify-content: center; align-items: center; gap: 14px; flex-wrap: wrap; }
.account-link[disabled] { opacity: 0.5; cursor: default; }
/* --red is ATTENTION, and the destructive confirm button already wears it (.confirm-yes-danger).
   This is the same job on the link that opens that dialog — no new colour, no new token. */
.account-link.danger { color: var(--red); }
.account-link { font-size: 12px; color: var(--muted); background: none; border: none; padding: 4px 8px; cursor: pointer; text-decoration: underline; text-underline-offset: 3px; font-family: 'DM Sans', sans-serif; }

.build-tag { margin: 0.5rem 0; text-align: center; font-family: 'DM Mono', monospace; font-size: 10px; letter-spacing: 0.05em; color: var(--muted2); opacity: 0.6; cursor: pointer; }

/* Only shown when a newer build is live but reloading now would interrupt a workout in progress. */
/* The gap between tapping Start and the logger being ready. Deliberately shaped like the exercise
   cards it is about to become rather than a spinner: it says "this is loading", not "something is
   happening somewhere". */
.opening-note { font-size: 13px; color: var(--muted); margin-bottom: 0.875rem; }
.opening-bar { height: 58px; border-radius: 12px; margin-bottom: 0.625rem; background: color-mix(in srgb, var(--text) 7%, transparent); animation: opening-pulse 1.4s ease-in-out infinite; }
.opening-bar:nth-child(3) { animation-delay: 0.15s; }
.opening-bar:nth-child(4) { animation-delay: 0.3s; }
@keyframes opening-pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.45; } }
@media (prefers-reduced-motion: reduce) { .opening-bar { animation: none; } }

/* z-index 1000, above #login-screen's 999: the login screen is a full-viewport opaque
   overlay, so a banner below it is invisible on the one screen where it is the only way out of a
   stale build. Above the 200/300 modals too, which it already was. */
.update-banner { position: fixed; left: 12px; right: 12px; bottom: 78px; z-index: 1000; display: flex; align-items: center; justify-content: space-between; gap: 10px; padding: 10px 14px; background: var(--bg3); border: 1px solid var(--accent); border-radius: 10px; font-size: 13px; font-weight: 600; color: var(--text); box-shadow: 0 6px 20px rgba(35,31,26,0.16); }
/* "Always offer" (31 Aug 2026) turned this from a bar that appeared for a second into one that can
   sit there for a whole session. The toast (bottom 85px) and the find bar (bottom 78px) share its
   slot and both lose to its z-index, so a permanent offer would quietly cover every toast the app
   shows — including the ones that name a failed write. They move up while it is on screen. */
body.has-update-banner .toast { bottom: 130px; }
body.has-update-banner .find-bar { bottom: 126px; }
.update-banner button { padding: 7px 14px; font-size: 12px; font-weight: 700; letter-spacing: 0.04em; text-transform: uppercase; color: #fff; background: var(--accent); border: none; border-radius: 7px; cursor: pointer; font-family: 'DM Sans', sans-serif; }

/* ─── THE APP'S OWN CONFIRM (19 Aug 2026) ──────────────────
   Replaces eight native confirm() calls. A browser dialog paints OS chrome and the origin name
   ("delpedro.github.io says") over a hand-built app, which is the same objection the native
   <select> got on 17 Aug — it is the one part of the app with no design language on it.
   Cancel sits left, the action sits right, and both say what they DO rather than OK/Cancel, so the
   button text is readable without the question. The action turns red on the destructive ones;
   that is the only place --red is used for anything but an error, and it is deliberate — green
   still means done, the accent still means go. */
.confirm-box { background: var(--bg2); border: 1px solid var(--border2); border-radius: 14px; padding: 1.25rem; max-width: 420px; margin: 12vh auto 0; }
.confirm-title { font-family: 'Bebas Neue', sans-serif; font-size: 20px; letter-spacing: 0.5px; color: var(--text); }
/* pre-line, so a list of exercise names keeps its line breaks without being built as markup. */
.confirm-body { font-size: 14px; color: var(--muted2); line-height: 1.55; white-space: pre-line; margin-top: 0.5rem; }
.confirm-actions { display: flex; gap: 0.625rem; margin-top: 1.25rem; }
/* Content-sized would let a long label like "Finish without them" squeeze Cancel to nothing. */
.confirm-actions .btn { flex: 1; min-width: 0; }
.confirm-yes-danger { background: var(--red); }
.confirm-yes-danger:active { background: #8e3428; }
/* askPrompt() (24 Aug 2026) — the confirm box with one field in it, replacing the last native
   prompt() in the app. No styles of its own beyond the spacing: .field-label and .field-input are
   the same two classes every other form in the app uses, so a typed exercise name looks like a
   typed anything-else rather than like a dialog bolted on. */
.confirm-field { margin-top: 0.875rem; }
.confirm-field .field-input { width: 100%; }

/* ─── ONBOARDING (22 Aug 2026) ────────────────────────────────────────────────
   Layout C off the contact sheet: one question a screen, the question set in Bebas at the size a
   heading gets, and the answer on a single accent-underlined line rather than in a boxed field.
   The box is what makes a form look like a form; this has to look like being asked something.

   z-index 900, below #login-screen's 999. An expired token while the form is open must put the
   login screen IN FRONT of it, not behind — see forceLogout(). */
html.ob-active { overflow: hidden; }
/* The panel is matched to the VISIBLE viewport while the keyboard is up (obViewportFit), so it no
   longer covers the whole screen — and the home screen showed through the strip underneath it and
   behind the keyboard's translucent toolbar. Nothing behind an eight-screen form should be visible
   at all: 23 Aug, "look at it closely, you see the home page behind it". */
html.ob-active #app { display: none; }

/* Not `inset: 0`: obViewportFit() overwrites top and height so the panel matches the VISIBLE
   viewport when the keyboard is up on the name screen. iOS never shrinks a fixed box itself. */
#onboarding { position: fixed; left: 0; right: 0; top: 0; height: 100%; z-index: 900; background: var(--bg); display: flex; flex-direction: column; }
.ob-top { display: flex; align-items: center; justify-content: space-between; padding: 0.7rem 1.1rem; border-bottom: 1px solid var(--border); }
.ob-brand { font-family: 'Bebas Neue', sans-serif; font-size: 20px; letter-spacing: 1px; color: var(--accent); display: flex; align-items: center; gap: 7px; line-height: 1; }
.ob-brand .brand-mark { width: 21px; height: 21px; }
.ob-date { font-size: 11px; color: var(--muted); }
/* The date and the way out share the right-hand end, so the brand keeps the left. */
.ob-top-right { display: flex; align-items: center; gap: 0.35rem; }
/* max-width so it doesn't stretch on the PC tab Del checks at phone width via F12 — the column is
   the same shape on both. */
.ob-body { flex: 1; width: 100%; max-width: 420px; margin: 0 auto; min-height: 0; display: flex; flex-direction: column; gap: 0.75rem; padding: 1.1rem calc(1.1rem + env(safe-area-inset-left)) calc(1.1rem + env(safe-area-inset-bottom)); }
.ob-rail { display: flex; gap: 6px; }
.ob-rail i { height: 3px; flex: 1; border-radius: 2px; background: var(--border2); }
.ob-rail i.on { background: var(--accent); }

/* Centred in the slack rather than stacked at the top: on a tall phone a top-aligned single
   question leaves two thirds of the screen empty and reads as a page that failed to load. */
/* Anchored to the top, not centred. Centring put the question in a different place on every screen
   — halfway down on the chip screens, near the top on the wheels — so it appeared to jump as you
   stepped through, with dead space above and below it. Same place all eight times reads as one
   designed flow rather than eight pages. */
.ob-wrap { flex: 1; min-height: 0; display: flex; flex-direction: column; justify-content: flex-start; gap: 0.45rem; padding: 1.5rem 0.15rem 0; overflow-y: auto; }
.ob-q { font-family: 'Bebas Neue', sans-serif; font-size: 31px; line-height: 1.02; letter-spacing: 0.4px; font-weight: 400; }
.ob-sub { font-size: 12.5px; color: var(--muted); }
/* The live conversion under the question. --muted2, not --accent: it is a readout, not something
   you can tap, and --accent has one job (see the palette note at the top of this file). */
.ob-conv { font-family: 'DM Mono', monospace; font-size: 11.5px; letter-spacing: 0.04em; color: var(--muted2); min-height: 1.2em; margin-bottom: 0.4rem; font-variant-numeric: tabular-nums; }

.ob-row { display: flex; align-items: baseline; }
/* DM Sans, not the Bebas Neue the rest of this screen wears, and it is the one deliberate
   inconsistency on it. Bebas Neue has NO lowercase glyphs — it draws every letter as a capital —
   so this box rendered "Charlie", "charlie" and "cHARLIE" identically and a caps-lock slip was
   invisible while typing. The first anyone saw of it was the home screen: "Good morning, cHARLIE"
   (Del, 27 Aug 2026). This is the only free-text answer in the form, so it is the only one where
   the exact characters matter and the only one that cannot wear a caps-only face — everything else
   is picked rather than typed (chips, a date picker, wheels), which is also why nothing else uses
   this class. 26px, not 34: DM Sans sets far wider than a condensed caps face and a long name ran
   off the row. */
.ob-big { width: 100%; background: none; border: none; border-bottom: 2px solid var(--accent); border-radius: 0; padding: 6px 2px; font-family: 'DM Sans', sans-serif; font-size: 26px; font-weight: 600; line-height: 1.15; letter-spacing: 0; color: var(--text); outline: none; }
.ob-big::placeholder { color: var(--border2); font-weight: 400; letter-spacing: 0; }
.ob-unit { font-size: 15px; color: var(--muted); margin-left: 8px; flex: 0 0 auto; }

/* The wheels. Every number and the date are picked, not typed — see OB_WHEEL in app.js.
   The row height here IS the unit of measurement: app.js reads the answer back as
   scrollTop / OB_ITEM, so 44px has to agree with OB_ITEM. */
.ob-wheels { position: relative; display: flex; align-items: stretch; gap: 0.4rem; margin-top: 0.3rem; }
.ob-wheel {
  flex: 1 1 0; height: 176px; padding: 66px 0; overflow-y: auto; text-align: center;
  scroll-snap-type: y mandatory; -webkit-overflow-scrolling: touch; scrollbar-width: none;
  touch-action: pan-y; outline: none;
  -webkit-mask-image: linear-gradient(to bottom, transparent, #000 28%, #000 72%, transparent);
          mask-image: linear-gradient(to bottom, transparent, #000 28%, #000 72%, transparent);
}
.ob-wheel::-webkit-scrollbar { display: none; }
.ob-wheel i {
  display: block; height: 44px; line-height: 44px; scroll-snap-align: center; font-style: normal;
  font-family: 'Bebas Neue', sans-serif; font-size: 26px; letter-spacing: 1px; color: var(--muted);
  font-variant-numeric: tabular-nums;
}
.ob-wheel i.on { color: var(--text); }
/* Until the wheel has been dragged it is showing a suggestion, not an answer, and it says so by
   staying grey. Tapping Next on an untouched wheel records nothing. */
.ob-wheels.unset .ob-wheel i.on { color: var(--muted); }
.ob-wheel:focus-visible { box-shadow: inset 0 0 0 2px var(--accent); border-radius: 8px; }
.ob-band {
  position: absolute; left: 0; right: 0; top: 66px; height: 44px; pointer-events: none;
  border-top: 1px solid var(--border2); border-bottom: 1px solid var(--border2);
  background: color-mix(in srgb, var(--accent) 7%, transparent);
}
.ob-wheel-unit { align-self: center; flex: 0 0 34px; padding-left: 8px; font-size: 15px; color: var(--muted); }
.ob-hint { font-family: 'DM Mono', monospace; font-size: 10px; letter-spacing: 0.08em; text-transform: uppercase; color: var(--muted); margin-top: 0.55rem; min-height: 1.2em; }

/* The pick-one screens. Small wrapping pills read as tags rather than as answers, and four of them
   wrapped 3 + 1 with an orphan on its own line — "horrible UI ... not professional at all", 23 Aug.
   A full-width row per option is what a phone does for a choice: nothing to aim for, nothing to
   wrap, and the list reads down the screen in one pass. */
.ob-opts { display: flex; flex-direction: column; gap: 0.5rem; margin-top: 0.6rem; }
.ob-opt {
  display: flex; align-items: center; justify-content: space-between; width: 100%;
  border: 1px solid var(--border2); background: var(--bg3); border-radius: 12px;
  padding: 15px 16px; font-size: 15px; color: var(--text); font-family: 'DM Sans', sans-serif;
  text-align: left; cursor: pointer;
}
.ob-opt:active { background: var(--bg); }
/* --accent, not --amber: this is "you can tap this / you tapped this", not identity. */
.ob-opt.on { border-color: var(--accent); color: var(--accent); background: color-mix(in srgb, var(--accent) 10%, transparent); font-weight: 600; }
.ob-tick { font-size: 15px; opacity: 0; }
.ob-opt.on .ob-tick { opacity: 1; }
/* Two to six days a week are labels one character long. Stacked full-width they would be six near
   empty bars down the screen, so those go in a row and get the display face, like the wheels. */
.ob-opts.row { flex-direction: row; gap: 0.4rem; }
.ob-opts.row .ob-opt { flex: 1 1 0; justify-content: center; padding: 16px 0; font-family: 'Bebas Neue', sans-serif; font-size: 22px; letter-spacing: 1px; }
.ob-opts.row .ob-tick { display: none; }

/* Reserves its line whether or not there is an error, so the question above never jumps when one
   appears. */
.ob-err { font-size: 12px; color: var(--red); min-height: 1.2em; margin-top: 0.6rem; }

.ob-foot { display: flex; align-items: center; justify-content: space-between; gap: 0.6rem; }
.ob-foot-left { display: flex; align-items: center; gap: 0.4rem; }
.ob-back { background: none; border: none; color: var(--muted); font-size: 13px; font-family: 'DM Sans', sans-serif; cursor: pointer; padding: 10px 8px 10px 0; }
.ob-count { font-family: 'DM Mono', monospace; font-size: 10.5px; letter-spacing: 0.08em; color: var(--muted); text-transform: uppercase; }
.ob-next { background: var(--accent); border: none; border-radius: 8px; padding: 12px 26px; color: #fff; font-size: 14.5px; font-weight: 600; font-family: 'DM Sans', sans-serif; cursor: pointer; }
.ob-next:active { transform: scale(0.98); background: #933f1c; }
.ob-next:disabled { opacity: 0.45; }

/* ─── THE OVERLOAD PANEL (7 Sept 2026) ────────────────────────────────────────
   Del's own design, named in the gym on 7 Sept: "a side modal for each exercise tile, that comes
   in from the side to contain overload information... we aim for best reps on the exercises to date
   versus the reps done last time". Cut J2 of a four-round contact sheet.

   ⛔ THERE IS NOT ONE BOX IN HERE, AND THAT IS THE WHOLE DESIGN. Rounds one to three were rejected
   — "they're not clean" — because every read-only figure had been given a border, so a panel of
   nine bordered cells sat directly on top of three real input boxes and everything on screen looked
   pressable. The numbers sit on the card in the same three columns as the inputs below them, and
   the only bordered thing in the block is still something you can actually type in.

   ⚠️ THE COLOUR RULE, applied exactly (see "ONE COLOUR, ONE JOB" at the top of this file):
     --accent  the TAB, and only the tab. It is the one thing here you can press.
     --sc      the BEAT column. A target that states and never invites a tap — the same job, and so
               the same token, as the "8–12 reps" and rest tags in .ex-pills. It inherits the
               session's identity colour from .exercise-block, so the panel is amber on an upper
               day and violet on a lower one without a line of JS.
     --muted   the LAST column. No verdict: it is what happened, not a good or bad thing.
   Terracotta on the numbers would have said "tap me" three times over a figure you cannot tap. */
/* ⚠️ THE BAND BLEEDS TO THE TILE EDGE, AND THAT IS WHY THE NEGATIVE MARGIN IS HERE.
   The panel and the tab are both positioned against this band, and an absolutely positioned child
   resolves "right: 0" against its container's PADDING BOX. While the band sat inside the block's
   own padding that was 14px short of the tile's edge, so the tab floated in the middle of the reps
   box — Del, 7 Sept: "move this new modal back to the edge of the tile". Pulling the band out to
   the edge and giving it its own right padding puts the tab flush with the tile AND keeps the set
   rows clear of it: an 18px lane against a 16px tab.
   The block's own padding went back to uniform in the same change. It carried an extra 16px on the
   right while the tab sat up at the pills row; the tab is centred on the set rows now and can never
   reach the pills, so the whole tile was holding a gap open for a collision that cannot happen. */
.rows-band { position: relative; margin-right: -0.875rem; padding-right: 18px; }

/* ─── AND ONE CONTAINER FURTHER IN, 8 Sept 2026 ───────────────────────────────
   The band holds everything you type into, warm-up rows included. `.work-rows` holds only the
   WORKING rows, and the overload panel is positioned against this — because a panel has to be
   exactly as tall as the rows it prints LAST and BEAT for. That is the same fix as the one above,
   one level in: the band was the right container right up until the band stopped being only working
   sets, and two warm-ups later it is five rows tall around three rows of figures.
   ⭐ It is also why the warm-ups stay reachable while the panel is open, which is correct — a
   warm-up is what you do before you consult a target.
   ⚠️ THE NEGATIVE MARGIN IS NOT DECORATION. An absolutely positioned child resolves `right: 0`
   against its own container's PADDING BOX. Nested inside the band's 18px padding, the tab would
   land 18px short — back in the middle of the reps box, which is exactly the bug fixed on 7 Sept.
   .work-rows re-spans that lane and carries it itself, and the two row groups stay the same width. */
.work-rows { position: relative; margin-right: -18px; padding-right: 18px; }
.overload-panel {
  position: absolute; inset: 0 0 0 12%; z-index: 2;
  background: var(--bg3); border-left: 2px solid var(--accent);
  border-radius: 8px 0 0 8px;
  box-shadow: -12px 0 20px color-mix(in srgb, var(--text) 9%, transparent);
  padding: 0 22px 0 4px;
  display: flex; flex-direction: column; justify-content: center;
  /* Parks off the band's right edge. .exercise-block carries overflow:hidden so this never widens
     the page — a panel that could push the body sideways would be a horizontal scroll on every
     exercise of every session. */
  transform: translateX(calc(100% + 30px));
  opacity: 0; pointer-events: none;
  transition: transform 0.22s cubic-bezier(0.2, 0.7, 0.25, 1), opacity 0.16s linear;
}
.exercise-block.overload-open .overload-panel { transform: translateX(0); opacity: 1; pointer-events: auto; }

/* The tab is the affordance AND the close button — it stays on screen while the panel is over the
   block, which is the reason the panel needs no ✕ of its own and no scrim behind it. Tapping the
   exercise name does the same thing (cut A1); this is the one you can see. */
.overload-tab {
  /* Centred against the rows band, so the control sits level with what it opens rather than
     floating somewhere up the tile. */
  position: absolute; right: 0; top: 50%; transform: translateY(-50%); z-index: 3;
  width: 16px; height: 58px; padding: 0;
  background: color-mix(in srgb, var(--accent) 14%, transparent);
  border: 1px solid color-mix(in srgb, var(--accent) 32%, transparent); border-right: none;
  border-radius: 8px 0 0 8px;
  color: var(--accent); font-size: 12px; font-family: 'DM Sans', sans-serif; line-height: 1;
  display: flex; align-items: center; justify-content: center; cursor: pointer;
}
.exercise-block.overload-open .overload-tab { background: var(--accent); border-color: var(--accent); color: #fff; }
.overload-chev { display: block; transition: transform 0.22s cubic-bezier(0.2, 0.7, 0.25, 1); }
.exercise-block.overload-open .overload-chev { transform: rotate(180deg); }

/* Same three columns as .set-row above, deliberately — the eye does not move between the panel and
   the boxes it is standing in for. */
/* P2: the first column is the LOAD, not the set number — the real set rows' numbers show through
   in the 12% gap the panel leaves on the left, so printing them again was a duplicate and the
   weight costs no width. 42px holds '47.5' and 'Band'. */
.ol-grid { display: grid; grid-template-columns: 42px 1fr 1fr; align-items: center; }
/* Two columns when the load column would carry nothing but dashes — a Side Plank has no weight and
   never will. See overloadPanelHtml(): a Farmers Walk is timed AND loaded, so the column stays
   there and only the unit moves onto the figures. */
.ol-grid-2 { grid-template-columns: 1fr 1fr; }
/* O3 (28 Sept): last time's RiR between LAST and BEAT. A fixed 30px — it only ever holds one badge
   ('4+' is the widest) — so LAST and BEAT split what is left and the 30px Bebas hero keeps its room
   at 358px. Dropped on timed holds, which never carry RiR. */
.ol-grid.ol-has-rir { grid-template-columns: 42px 1fr 30px 1fr; }
.ol-grid-2.ol-has-rir { grid-template-columns: 1fr 30px 1fr; }
.ol-grid > * { padding: 3px 0; text-align: center; }
/* The reps box's own badge at its own size, taken out of the absolute position it has in there. */
.ol-rir .rir-badge { position: static; transform: none; display: inline-block; }
.ol-cap {
  font-family: 'DM Mono', monospace; font-size: 8.5px; letter-spacing: 0.13em;
  text-transform: uppercase; color: var(--muted);
}
.ol-cap-beat { color: var(--sc); }
.ol-kg { font-family: 'DM Mono', monospace; font-size: 12.5px; color: var(--muted2); font-variant-numeric: tabular-nums; }
/* Last time at a load the Kg column is not naming — reads '70×13', so it never has to be inferred. */
.ol-last-load { font-size: 12.5px; }
.ol-set { font-family: 'DM Mono', monospace; font-size: 12px; color: var(--muted); }
.ol-n { font-family: 'DM Mono', monospace; font-size: 15px; font-variant-numeric: tabular-nums; }
.ol-last { color: var(--muted); }
.ol-beat { color: var(--sc); }
/* N3 — the sets he has ALREADY MATCHED. Green is "on target" everywhere else in BenchBear and it means
   the same thing here: this figure is not out there any more, he is level with it. Marked per set,
   so a lift where he matched two and dropped one names the set with a rep still in it. Del's Seated
   Calf Raise goes green on all three, which is the correct thing for the app to be telling him and
   is what the two identical columns were failing to say. */
.ol-eq { color: var(--green); }
/* Set 1 decides whether the session moved, so it takes the size. It is not a second element sitting
   above the grid — it IS the grid's own set-1 figure, which is why the panel stays one object. */
.ol-hero { font-family: 'Bebas Neue', sans-serif; font-size: 30px; line-height: 0.8; letter-spacing: 0.5px; }
.ol-foot {
  font-family: 'DM Mono', monospace; font-size: 8.5px; letter-spacing: 0.13em;
  text-transform: uppercase; color: var(--muted); text-align: center; margin-top: 9px;
}

@media (prefers-reduced-motion: reduce) {
  .overload-panel, .overload-chev { transition: none; }
}

/* ─── RIR — REPS IN RESERVE (28 Sept 2026) ────────────────────────────────────
   MacroFactor's pattern, picked as M2 + L2: a chip strip that rides on top of the keyboard while a
   working set's reps are typed, and a badge inside the reps box once a value is picked. No switch,
   no extra column — see the RIR block in js/app.js.

   THE BADGE COLOURS ARE A SCALE, AND DEL PICKED THEM KNOWING THE CLASH. 0 wears --red, which
   elsewhere means "went the wrong way"; 3 wears --green, which elsewhere means "done". He chose the
   coloured badge (L2) over a grey tag (L1) off the sheet with that written under it. The colour
   here reads as "how close to failure", 0 hottest, and lives only inside the reps box. */
.reps-cell { position: relative; min-width: 0; }
.rir-badge {
  position: absolute; right: 5px; top: 50%; transform: translateY(-50%);
  min-width: 18px; padding: 1px 5px; border-radius: 8px;
  font-family: 'DM Mono', monospace; font-size: 10px; line-height: 1.4; text-align: center;
  color: #fff; pointer-events: none;
}
.rir-badge:empty { display: none; }
.rir-badge.rir-0 { background: var(--red); }
.rir-badge.rir-1 { background: #c2702a; }
.rir-badge.rir-2 { background: var(--amber); }
.rir-badge.rir-3 { background: var(--green); }
.rir-badge.rir-4 { background: var(--blue); }
/* H2: the History card's per-set strip. The same badge, out of the reps box and shrunk to sit on
   the 10px sub-line; the dash holds the place of a set with no pick. */
.pf-rir { display: inline-flex; gap: 2px; vertical-align: middle; }
.pf-rir .rir-badge { position: static; transform: none; min-width: 13px; padding: 0 3px; font-size: 8.5px; border-radius: 6px; }
.pf-rir .rir-badge.rir-none { background: var(--border2); color: var(--muted); }

/* The dock. `top: 0` plus a translateY set by rirPlaceDock() to the visual viewport's bottom edge,
   which is the top of the keyboard — `bottom: 0` would put it behind the keys on iOS. */
.rir-dock {
  position: fixed; top: 0; left: 0; right: 0; z-index: 900;
  background: var(--bg2); border-top: 1px solid var(--border);
  box-shadow: 0 -4px 12px color-mix(in srgb, var(--text) 8%, transparent);
  padding: 8px 12px 9px;
}
.rir-dock-cap {
  display: flex; justify-content: space-between; margin-bottom: 6px;
  font-size: 10px; font-weight: 600; letter-spacing: 0.04em; text-transform: uppercase; color: var(--muted);
}
.rir-dock-row { display: flex; gap: 8px; max-width: 520px; margin: 0 auto; }
.rir-dock-row .rir-chip { height: 34px; border-radius: 17px; font-family: 'DM Mono', monospace; font-size: 13px; padding: 0; }
