/* pages.css — per-screen layout. */

/* ---- tab bar -------------------------------------------------------------- */

.tabbar {
  position: sticky;
  bottom: 0;
  flex: none;
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  border-top: 1px solid var(--color-border);
  background: var(--color-ink);
  padding-bottom: var(--inset-bottom);
}

.tabbar button {
  /* The bar adds the home-indicator inset below these, so the buttons hand their
     own bottom padding over to it: 12px above the label and 12+34 below read as
     a label stranded at the top of a tall empty bar. Clamped at zero so a device
     with no inset — Android, desktop, Safari with its toolbar showing — keeps the
     symmetric padding it needs. The 34px itself stays: it is where the home
     indicator lives, and a button under it loses its taps to the system swipe. */
  padding: var(--space-3) 0 max(0px, calc(var(--space-3) - var(--inset-bottom)));
  font-family: var(--font-mono);
  font-size: var(--text-xs);
  letter-spacing: .1em;
  text-transform: uppercase;
  color: var(--color-text-dim);
}

.tabbar button.is-active { color: var(--color-text); }

.tabbar button.is-active::before {
  content: "";
  display: block;
  width: 22px;
  height: 3px;
  border-radius: var(--radius-pill);
  background: var(--color-primary);
  margin: 0 auto var(--space-2);
}

/* ---- date navigation ------------------------------------------------------ */

.datenav {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  flex: none;
}

.datenav-label {
  flex: 1;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 2px;
}

.datenav-label b {
  font-family: var(--font-mono);
  font-size: var(--text-lg);
  letter-spacing: -.02em;
}

.datenav-label small {
  font-family: var(--font-mono);
  font-size: var(--text-xs);
  letter-spacing: .08em;
  text-transform: uppercase;
  color: var(--color-text-dim);
}

/* ---- add-food entry point ------------------------------------------------- */

.fab-stack {
  position: fixed;
  right: var(--space-4);
  bottom: calc(72px + var(--tabbar-extra));
  z-index: 12;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--space-3);
}

.fab {
  width: 56px;
  height: 56px;
  border-radius: var(--radius-pill);
  background: var(--color-primary);
  color: #fff;
  font-size: var(--text-2xl);
  line-height: 1;
  display: grid;
  place-items: center;
  box-shadow: var(--shadow-md);
}

/* Same blue as the add button — both are primary actions. Size alone carries the
   hierarchy: adding food is the main path, scanning is the shortcut. */
.fab-secondary {
  width: 46px;
  height: 46px;
}

/* Log is a panel over Today rather than a separate destination, so adding food
   never loses your place on the day you're looking at. */
.log-panel {
  position: fixed;
  inset: 0;
  z-index: 15;
  background: var(--color-bg);
  overflow-y: auto;
  /* Covers the screen, so a chained scroll would move Today invisibly and leave
     you somewhere else when the panel closes. */
  overscroll-behavior: contain;
  padding: calc(var(--inset-top) + var(--space-3)) var(--space-4)
           calc(var(--inset-bottom) + var(--space-12));
}

/* ---- daily readout -------------------------------------------------------- */

.readout { gap: var(--space-3); }

.readout-head {
  display: flex;
  align-items: flex-end;
  justify-content: space-between;
  gap: var(--space-3);
}

.readout-total {
  font-family: var(--font-mono);
  font-variant-numeric: tabular-nums;
  font-size: var(--text-3xl);
  line-height: .95;
  letter-spacing: -.035em;
}

.readout-of {
  font-family: var(--font-mono);
  font-size: var(--text-xs);
  letter-spacing: .14em;
  text-transform: uppercase;
  color: var(--color-text-dim);
  margin-top: var(--space-1);
}

.readout-remaining { text-align: right; }

.readout-remaining b {
  display: block;
  font-family: var(--font-mono);
  font-variant-numeric: tabular-nums;
  font-size: var(--text-xl);
  color: var(--color-success);
}

.readout-remaining.is-over b { color: var(--color-danger); }

.readout-remaining small {
  font-family: var(--font-mono);
  font-size: var(--text-xs);
  letter-spacing: .12em;
  text-transform: uppercase;
  color: var(--color-text-dim);
}

/* Stacked calorie bar, segmented by macro. */
.track {
  display: flex;
  gap: 3px;
  height: 7px;
  background: var(--color-ink);
  border-radius: var(--radius-pill);
  overflow: hidden;
}
.track i { display: block; height: 100%; border-radius: var(--radius-pill); }

.macros { display: flex; flex-direction: column; gap: var(--space-2); }

.macro { display: flex; align-items: center; gap: var(--space-2); }

/* On Today the rows are buttons — they open the who-contributed sheet — so the
   reset keeps them looking exactly like the plain rows everywhere else. */
button.macro {
  background: none;
  border: 0;
  padding: 0;
  width: 100%;
  font: inherit;
  color: inherit;
  text-align: left;
  cursor: pointer;
}

.macro-name {
  font-family: var(--font-mono);
  font-size: var(--text-xs);
  letter-spacing: .1em;
  text-transform: uppercase;
  color: var(--color-text-muted);
  width: 62px;
  flex: none;
}

.macro-bar {
  flex: 1;
  display: flex;               /* the eaten segment, then the plan's ghost */
  height: 5px;
  background: var(--color-ink);
  border-radius: var(--radius-pill);
  overflow: hidden;
}
.macro-bar i { display: block; flex: none; height: 100%; border-radius: var(--radius-pill); }

/* The plan, wherever it stacks: same hue, clearly not yet real. */
.macro-bar i.is-planned,
.rack i.is-planned { opacity: .35; }

.plan-line {
  font-family: var(--font-mono);
  font-size: var(--text-xs);
  letter-spacing: .04em;
  color: var(--color-text-muted);
}
.plan-line.is-over { color: var(--color-danger); }

.row.is-planned { opacity: .7; }
.row.is-planned .row-text b::after {
  content: " · planned";
  font-weight: 400;
  font-size: var(--text-xs);
  color: var(--color-text-dim);
}

.macro-val {
  font-family: var(--font-mono);
  font-variant-numeric: tabular-nums;
  font-size: var(--text-sm);
  width: 74px;
  text-align: right;
  flex: none;
}
.macro-val s { color: var(--color-text-dim); text-decoration: none; }

/* ---- meal sections -------------------------------------------------------- */

.section-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  font-family: var(--font-mono);
  font-size: var(--text-xs);
  letter-spacing: .16em;
  text-transform: uppercase;
  color: var(--color-text-dim);
  padding-top: var(--space-2);
}

.empty {
  color: var(--color-text-dim);
  font-size: var(--text-sm);
  text-align: center;
  padding: var(--space-8) var(--space-4);
}

/* ---- log screen ----------------------------------------------------------- */

.search-row {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  flex: none;
}

.search {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  background: var(--color-surface);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-pill);
  padding: 0 var(--space-4);
  flex: 1;
  min-width: 0;
  box-shadow: var(--shadow-sm);
}

/* ---- viewfinder ----------------------------------------------------------- */

.viewfinder {
  position: relative;
  background: var(--color-ink);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-lg);
  overflow: hidden;
  aspect-ratio: 4 / 3;
}

.viewfinder video {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
}

/* Framing guide only — the whole frame is decoded, so a barcode slightly
   outside the box still reads. */
.reticle {
  position: absolute;
  inset: 26% 10%;
  border: 2px solid rgba(244, 239, 232, .55);
  border-radius: var(--radius-md);
  pointer-events: none;
}

.search input {
  flex: 1;
  background: none;
  border: 0;
  outline: none;
  padding: var(--space-3) 0;
  /* Not --text-sm: 13.6px is under the 16px iOS zoom threshold, and this is the
     field most likely to be typed in first. See the floor in base.css. */
  font-size: 16px;
}
.search input::placeholder { color: var(--color-text-dim); }

.chips { display: flex; gap: var(--space-2); flex-wrap: wrap; }

.chip {
  padding: var(--space-2) var(--space-3);
  border-radius: var(--radius-pill);
  border: 1px solid var(--color-border);
  font-family: var(--font-mono);
  font-size: var(--text-xs);
  letter-spacing: .08em;
  text-transform: uppercase;
  color: var(--color-text-muted);
}

.chip.is-active {
  border-color: var(--color-primary);
  background: var(--color-primary-soft);
  color: var(--color-text);
}

/* The two-tap target. Large enough to hit without looking. */
.tap {
  width: 40px;
  height: 40px;
  flex: none;
  border-radius: var(--radius-pill);
  border: 1px solid var(--color-border);
  background: var(--color-surface-raised);
  color: var(--color-text-muted);
  display: grid;
  place-items: center;
  font-size: var(--text-lg);
  line-height: 1;
}

.tap-primary {
  background: var(--color-primary);
  border-color: var(--color-primary);
  color: #fff;
}

/* ---- quantity sheet ------------------------------------------------------- */

/* An open sheet owns the screen, so the page behind it stops being scrollable.
 *
 * Without this the page keeps its full scroll range underneath, and every
 * gesture the sheet cannot use itself falls through to it: a sheet shorter than
 * its own content box has nothing to scroll, so dragging it scrolled the page
 * instead — and so did dragging the dimmed strip beside it, which is not a
 * scroll container at all and never could be fixed with overscroll-behavior.
 *
 * :has() rather than a class toggled in JS because all nineteen sheets are
 * `x-if` guarded: the backdrop is absent from the DOM when closed, so its mere
 * presence is already the signal, and no open/close path can forget to say so.
 * The scroll position survives — overflow: hidden on the scrolling element
 * stops the scroll without discarding scrollTop. */
html:has(.sheet-backdrop) { overflow: hidden; }

.sheet-backdrop {
  position: fixed;
  inset: 0;
  background: rgba(0, 0, 0, .6);
  display: flex;
  align-items: flex-end;
  z-index: 20;
  /* The keyboard covers the bottom of the screen, which is where sheets live.
     Published by trackKeyboardInset() in app.js; 0 whenever no keyboard is up,
     and on any browser without visualViewport. */
  padding-bottom: var(--keyboard-inset, 0px);
}

.sheet {
  width: 100%;
  background: var(--color-surface);
  border-top: 1px solid var(--color-border);
  border-radius: var(--radius-lg) var(--radius-lg) 0 0;
  padding: var(--space-4) var(--space-4) calc(var(--space-6) + var(--inset-bottom));
  display: flex;
  flex-direction: column;
  gap: var(--space-4);
  /* Percentage of the backdrop rather than vh, so the keyboard padding above
     shortens the sheet instead of pushing its bottom off the screen. */
  max-height: 88%;
  overflow-y: auto;
  /* Reaching the end of a sheet is the end of the gesture, not the start of
     scrolling whatever is behind it. */
  overscroll-behavior: contain;
}

/* A search sheet holds its own chrome still and scrolls only its results.
 *
 * The whole sheet used to be the scroll region: 3,288px of it against a 668px
 * window with a full library loaded. So the field being typed into scrolled away
 * from the results it was filtering, and Cancel sat at the far end of the list
 * rather than under your thumb. The grabber travelled with them, which disabled
 * swipe-to-dismiss too, because $swipe only engages at scrollTop 0 — one scroll
 * of the list and every way out of the sheet was somewhere off-screen.
 *
 * Both widths, not just phones: the centred desktop dialog had the same problem,
 * it was simply less obvious at 82vh. */
.sheet-backdrop.is-search .sheet { overflow: hidden; }

.sheet-backdrop.is-search .rows {
  flex: 1;
  /* Without this a flex item refuses to shrink below its content, so the list
     grows the sheet instead of scrolling inside it. */
  min-height: 0;
  overflow-y: auto;
  overscroll-behavior: contain;
}

/* Everything that is not the list keeps its height while the list takes the
   slack. */
.sheet-backdrop.is-search .search,
.sheet-backdrop.is-search .grabber,
.sheet-backdrop.is-search .card-label,
.sheet-backdrop.is-search .btn { flex: none; }

.grabber {
  width: 36px;
  height: 4px;
  border-radius: var(--radius-pill);
  background: var(--color-border);
  margin: 0 auto;
  flex: none;
}

.qty { display: flex; align-items: center; gap: var(--space-3); }

.qty-step {
  width: 48px;
  height: 48px;
  flex: none;
  border-radius: var(--radius-pill);
  border: 1px solid var(--color-border);
  background: var(--color-surface-raised);
  color: var(--color-text-muted);
  font-size: var(--text-xl);
  display: grid;
  place-items: center;
}

/* The fast way to an amount, next to the exact one.
 *
 * Styled rather than left to the browser because the default thumb is around
 * 12px, and this is the one control on the screen you drag with a thumb. The
 * track keeps its own height for contrast; the hit area comes from the padding,
 * which is why the element is taller than the line it draws. */
.qty-slider {
  -webkit-appearance: none;
  appearance: none;
  width: 100%;
  background: none;
  /* 20px either side of a 4px track is a 44px target. --space-3 gave 28, which
     is under the minimum and measurably so — the thumb looked right and the
     area you could actually hit did not. */
  padding: 20px 0;
  margin: 0;
  touch-action: none;             /* the drag is the gesture, not a page scroll */
}

.qty-slider::-webkit-slider-runnable-track {
  height: 4px;
  border-radius: var(--radius-pill);
  background: var(--color-border);
}

.qty-slider::-moz-range-track {
  height: 4px;
  border-radius: var(--radius-pill);
  background: var(--color-border);
}

.qty-slider::-webkit-slider-thumb {
  -webkit-appearance: none;
  appearance: none;
  width: 24px;
  height: 24px;
  margin-top: -10px;              /* centred on a 4px track */
  border-radius: 50%;
  background: var(--color-primary-hover);
  border: 2px solid var(--color-surface);
  box-shadow: var(--shadow-md);
  cursor: grab;
}

.qty-slider::-moz-range-thumb {
  width: 24px;
  height: 24px;
  border-radius: 50%;
  background: var(--color-primary-hover);
  border: 2px solid var(--color-surface);
  box-shadow: var(--shadow-md);
  cursor: grab;
}

.qty-slider:active::-webkit-slider-thumb { cursor: grabbing; }

/* ---- weigh-in wheel --------------------------------------------------------
 *
 * The numbers are the control: they spin past a fixed centre line, native
 * picker style. Scroll snap does the physics; the mask fades the ends so the
 * column reads as a drum rather than a list. */

.wheel {
  position: relative;
  height: 210px;
}

.wheel-scroll {
  height: 100%;
  overflow-y: auto;
  scroll-snap-type: y mandatory;
  overscroll-behavior: contain;   /* a hard flick must not scroll the sheet */
  scrollbar-width: none;
  -webkit-mask-image: linear-gradient(transparent, #000 32%, #000 68%, transparent);
  mask-image: linear-gradient(transparent, #000 32%, #000 68%, transparent);
}

.wheel-scroll::-webkit-scrollbar { display: none; }

/* Lets the first and last number reach the centre line. */
.wheel-pad { height: calc((210px - 42px) / 2); }

.wheel-item {
  height: 42px;
  line-height: 42px;
  text-align: center;
  scroll-snap-align: center;
  font-size: 1.15rem;
  font-variant-numeric: tabular-nums;
  color: var(--color-text-muted);
  opacity: .55;
  transition: color 120ms, opacity 120ms, transform 120ms;
  cursor: pointer;
}

.wheel-item.is-centred {
  color: var(--color-text);
  opacity: 1;
  font-weight: 650;
  transform: scale(1.12);
}

.wheel-centre {
  position: absolute;
  inset: 50% 18% auto;
  height: 42px;
  transform: translateY(-50%);
  border-top: 1px solid var(--color-border);
  border-bottom: 1px solid var(--color-border);
  pointer-events: none;
}

/* ---- nutrition label ------------------------------------------------------ */

/* Deliberately the FDA panel's typography rather than this app's: the rules,
   the weights and the hanging percentages are what make it recognisable as a
   label at a glance. It is quoting a thing that exists, so it should look like
   the thing. Dark, because everything else here is. */
.nutrition {
  border: 1px solid var(--color-border);
  border-radius: var(--radius-md);
  background: var(--color-surface-raised);
  padding: var(--space-3) var(--space-4);
  font-family: var(--font-sans);
  font-size: var(--text-sm);
}

.nf-title {
  font-size: var(--text-2xl);
  font-weight: 800;
  letter-spacing: -.02em;
  line-height: 1.1;
}

.nf-serving {
  display: flex;
  justify-content: space-between;
  gap: var(--space-3);
  padding-bottom: var(--space-2);
  /* The thick rule under the serving is the label's own signature. */
  border-bottom: 8px solid var(--color-border);
  color: var(--color-text-muted);
}

/* The source's own phrase for a serving, under the amount. Quieter than the
   line above it: that one is what is being logged, this is a description. */
.nf-serving-text {
  border-top: 0;
  padding-top: 0;
  color: var(--color-text-muted);
  font-weight: 400;
}

/* The signature rule belongs under the serving block as a whole. With the
   description present that is two lines, so the first one gives its rule up and
   the second carries it — otherwise the label appears to have two headers.
   :has() is already how .app-bar and the sheet-backdrop lock work here. */
.nf-serving:has(+ .nf-serving-text) {
  border-bottom: 0;
  padding-bottom: 2px;
}

.nf-calories {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  padding: var(--space-2) 0;
  border-bottom: 4px solid var(--color-border);
  font-weight: 800;
}

.nf-calories b {
  font-family: var(--font-mono);
  font-size: var(--text-2xl);
  font-variant-numeric: tabular-nums;
}

.nf-dv {
  text-align: right;
  font-weight: 700;
  font-size: var(--text-xs);
  padding: var(--space-1) 0;
  border-bottom: 1px solid var(--color-border);
}

.nf-row {
  display: flex;
  justify-content: space-between;
  gap: var(--space-3);
  padding: var(--space-1) 0;
  border-bottom: 1px solid var(--color-border);
}

/* Indent carries the nesting — saturated fat under total fat, added sugars
   under total sugars — which is how a label shows what is part of what. */
.nf-in-1 { padding-left: var(--space-4); }
.nf-in-2 { padding-left: var(--space-8); }

.nf-row.is-major .nf-name > span:first-child { font-weight: 700; }

.nf-name { display: flex; gap: var(--space-2); min-width: 0; }

.nf-amount {
  font-family: var(--font-mono);
  font-variant-numeric: tabular-nums;
  color: var(--color-text-muted);
}

.nf-pct {
  flex: none;
  font-family: var(--font-mono);
  font-variant-numeric: tabular-nums;
  font-weight: 700;
}

/* A label line that opens the who-contributed sheet. The reset keeps the row's
   own layout in charge; the class only exists on the day label, where every
   line answers a tap — a food's own label stays inert. */
button.nf-tap {
  width: 100%;
  background: none;
  border: 0;
  border-bottom: 1px solid var(--color-border);
  color: inherit;
  font: inherit;
  text-align: inherit;
  cursor: pointer;
}
button.nf-tap:active { background: var(--color-hover); }

/* The heavy rule between the macros and the micronutrients. */
.nf-rule {
  height: 8px;
  background: var(--color-border);
  margin: var(--space-2) 0;
}

.nf-note {
  font-size: var(--text-xs);
  color: var(--color-text-dim);
  padding-top: var(--space-2);
}

.qty-amount { flex: 1; text-align: center; }

.qty-amount input {
  width: 100%;
  background: none;
  border: 0;
  outline: none;
  text-align: center;
  font-family: var(--font-mono);
  font-variant-numeric: tabular-nums;
  font-size: var(--text-2xl);
  letter-spacing: -.02em;
}

.qty-amount small {
  display: block;
  font-family: var(--font-mono);
  font-size: var(--text-xs);
  letter-spacing: .14em;
  text-transform: uppercase;
  color: var(--color-text-dim);
}

.prefill {
  font-family: var(--font-mono);
  font-size: var(--text-xs);
  letter-spacing: .06em;
  text-transform: uppercase;
  color: var(--color-carbs);
  text-align: center;
}

/* ---- forms ---------------------------------------------------------------- */

/* min-width: 0 because a .field is usually a grid or flex item, and those
   default to min-width: auto — which for an <input> is its intrinsic size=20
   width, about 180px. Two of those in a two-column .field-grid cannot fit a
   390px phone, so the row overflowed and the right-hand field ran off the
   screen. The input is told to fill its field rather than size to itself. */
.field { display: flex; flex-direction: column; gap: var(--space-1); min-width: 0; }

/* Suggestion chips under a field's input. The field's 4px gap is right for a
   label hugging its box, but a row of tappable chips at 4px reads as part of
   the input — give them the step down that separates control from suggestion. */
.field .chips { margin-top: var(--space-1); }
.field input, .field select, .field textarea { min-width: 0; width: 100%; }

/* The notes box shares the input's clothes; resize stays off because a sheet
   that scrolls plus a corner drag-handle fight over the same gesture. */
.field textarea {
  background: var(--color-surface-raised);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-sm);
  padding: var(--space-3);
  outline: none;
  resize: none;
  font-family: inherit;
}
.field textarea:focus { border-color: var(--color-primary); }

/* An exercise's note on its workout card: a cue, quieter than the name, louder
   than the previous-session line under it. */
.ex-note {
  font-size: var(--text-xs);
  color: var(--color-text-muted);
  margin-top: 2px;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.field label {
  font-family: var(--font-mono);
  font-size: var(--text-xs);
  letter-spacing: .12em;
  text-transform: uppercase;
  color: var(--color-text-dim);
}

.field input {
  background: var(--color-surface-raised);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-sm);
  padding: var(--space-3);
  outline: none;
}
.field input:focus { border-color: var(--color-primary); }

/* The basis line: "The numbers below are for [100] [g] of this food." The two
   inputs are sized to their content rather than stretched across a grid, so the
   row reads as a sentence instead of as two unrelated form fields. */
.basis-row {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  flex-wrap: wrap;
}

.basis-row input[type="number"] { width: 5.5rem; flex: none; }
.basis-row input[type="text"]   { width: 6.5rem; flex: none; }

.basis-row span {
  font-size: var(--text-sm);
  color: var(--color-text-muted);
}

.field-grid {
  display: grid;
  grid-template-columns: repeat(2, 1fr);
  gap: var(--space-3);
}

/* ---- toast ---------------------------------------------------------------- */
/* Confirms a log landed without blocking the next one. */

.toast {
  position: fixed;
  left: 50%;
  transform: translateX(-50%);
  bottom: calc(76px + var(--tabbar-extra));
  z-index: 30;
  max-width: calc(100% - var(--space-8));
  padding: var(--space-2) var(--space-4);
  border-radius: var(--radius-pill);
  background: var(--color-surface-raised);
  border: 1px solid var(--color-border);
  box-shadow: var(--shadow-md);
  font-family: var(--font-mono);
  font-size: var(--text-xs);
  letter-spacing: .06em;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

/* ---- training -------------------------------------------------------------- */

.timer {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--space-2);
  background: var(--color-surface-raised);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-md);
  padding: var(--space-3) var(--space-4);
  flex: none;
  box-shadow: var(--shadow-inset), var(--shadow-md);
}

.timer .t {
  font-family: var(--font-mono);
  /* Sized to the viewport, not fixed. Three chips and two clocks share this row,
     and past an hour the elapsed readout gains a whole field — enough to push
     the rest clock out of the bar on a small phone. Scaling by width keeps the
     worst case inside without ever changing height for a reason the user did
     something, so nothing shifts. */
  font-size: clamp(var(--text-lg), 5.2vw, var(--text-2xl));
  letter-spacing: -.03em;
  font-variant-numeric: tabular-nums;
}

/* The rest controls live in the timer bar. They are shorter than the elapsed
   readout beside them, so showing them changes nothing about the bar's height —
   which is the whole point, since they appear the instant a set is checked and
   anything that moved would move the cards being read. */
.timer .rest-controls {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  /* Never wrap. A second line of chips would grow the bar and put the shift
     straight back. */
  flex-wrap: nowrap;
  flex: none;
  /* The bar aligns on the baseline for the numbers either side; chips have no
     number to sit on and would hang below it. */
  align-self: center;
}

/* Tighter than a normal chip. Three of these share the row with two readouts,
   and the elapsed clock grows a whole field wide once a workout passes an hour —
   the uppercase tracking alone was costing more than the gap between them. */
.timer .rest-controls .chip {
  white-space: nowrap;
  padding: var(--space-2) var(--space-2);
  letter-spacing: 0;
}

.timer .rest { text-align: right; }

.timer .rest b {
  display: block;
  font-family: var(--font-mono);
  font-size: clamp(var(--text-lg), 4.2vw, var(--text-xl));
  font-variant-numeric: tabular-nums;
  color: var(--color-carbs);      /* the only thing counting down */
}

.timer small {
  font-family: var(--font-mono);
  font-size: var(--text-xs);
  letter-spacing: .13em;
  text-transform: uppercase;
  color: var(--color-text-dim);
}

.prev {
  font-family: var(--font-mono);
  font-size: var(--text-xs);
  letter-spacing: .06em;
  text-transform: uppercase;
  color: var(--color-text-dim);
  padding-top: 4px;
}

/* The gap is what keeps consecutive finished sets apart. A hairline is invisible
   against the green — three checked sets in a row read as one block and you
   could not count them — and this is the number the eye actually uses. */
.setgrid {
  display: flex;
  flex-direction: column;
  gap: var(--space-1);
}

/* ---- reorderable exercise cards ------------------------------------------- */

/* The stack $dragCard measures. It owns the gap so the drag can assume one
   constant stride between cards — matching .app-main's, since the cards used to
   be its direct children and the spacing should not have changed. */
.cardstack {
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
}

/* Siblings glide into the gap; the dragged card follows the finger exactly and
   must not be animated, or it lags behind by the transition duration. */
.cardstack > .card { transition: transform var(--transition-fast); }

.cardstack > .card.is-dragging {
  transition: none;
  position: relative;
  z-index: 5;
  box-shadow: var(--shadow-lg);
  border-color: var(--color-primary-soft);
}

/* A routine in reorder mode. It is a .card so it inherits the stack's lift and
   glide, but lays out as a row: a name and a handle is all there is to show
   when the only question being asked is what order these go in. */
.cardstack > .card.row-card {
  flex-direction: row;
  align-items: center;
  gap: var(--space-3);
  padding: var(--space-3) var(--space-4);
  /* Every row in the stack is exactly this tall, headings included. $dragCard
     measures one stride from the first two cards and applies it to every slot,
     so a heading that sized to its own smaller text would make every index past
     it drift by the difference — the card would land one place off, and further
     off the more headings it passed. */
  min-height: 52px;
}

/* A category heading in the stack. Not a card to look at — no border, no lift,
   and flush with the section edge — but exactly a card's height, per above. */
.cardstack > .card.row-card.is-group {
  background: none;
  border-color: transparent;
  box-shadow: none;
  padding-left: 0;
  padding-right: 0;
}

.cardstack > .card.row-card.is-group small {
  font-family: var(--font-mono);
  font-size: var(--text-xs);
  letter-spacing: .12em;
  text-transform: uppercase;
  color: var(--color-text-muted);
}

/* The handle and the name. They sit in a wrapper now, because the row's contents
   are one branch of an x-if and a template renders a single child. */
.row-drag {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  flex: 1;
  min-width: 0;
}

.grip {
  flex: none;
  width: 28px;
  align-self: stretch;
  display: grid;
  place-items: center;
  color: var(--color-text-dim);
  font-size: var(--text-lg);
  line-height: 1;
  cursor: grab;
  /* The drag is driven by pointer events, which need the browser to stop
     claiming vertical drags for scrolling. Also set in JS, belt and braces. */
  touch-action: none;
}

.cardstack > .card.is-dragging .grip { cursor: grabbing; }

@media (prefers-reduced-motion: reduce) {
  .cardstack > .card { transition: none; }
}

/* Four tracks, not five: the remove button is gone and a left swipe reveals it
   instead, so nothing sits to the right of the checkmark any more. */
/* Headings are labels, not content: one that outgrows its column should back
   down rather than run across the next one — "PREVIOUS" was printing over "LB". */
.sethead span {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.sethead,
.setrow {
  display: grid;
  /* The fifth track is the plate maths.
   *
   * A fixed width, not `auto`. The header and every row are separate grids, so
   * `auto` resolved separately in each: the labels drifted off their columns and
   * a row showing "45" sat a dozen pixels wide of one showing "45x4". A load
   * needing two different plate sizes wraps inside the track instead, which
   * costs that one row some height and keeps every column true.
   *
   * The weight and reps tracks carry a floor too: as bare fr units they gave up
   * width to the plates until a three-digit weight was clipped to two. */
  /* Prev is 40px flat, not a fraction, so a set row reads the same width on
     every card. "135x8" measures 36, and an exact fit still ellipsises on
     sub-pixel rounding — which cost the reps, the half you cannot infer from the
     box alongside. The plate track is 40 too: the chips stack, so it only ever
     has to hold the widest single one. */
  grid-template-columns: 22px 40px minmax(44px, 1fr) minmax(36px, 1fr) 40px 38px;
  /* Tighter than it was. Six tracks on a 360px phone, and the gaps were costing
     the Prev column more than the plate chips did. */
  gap: var(--space-1);
  align-items: center;
  padding: var(--space-2) var(--space-2);
}

.sethead {
  font-family: var(--font-mono);
  font-size: var(--text-xs);
  letter-spacing: .13em;
  text-transform: uppercase;
  color: var(--color-text-dim);
  border-bottom: 1px solid var(--color-border);
}

/* Each set is its own rounded chip, separated by the grid's gap.
 *
 * The hairline this replaces could not do the job once a finished set went
 * green: 1px of --color-border laid over that tint is invisible, so a run of
 * checked sets read as one slab. The gap does the separating and the radius is
 * what makes each row read as a thing rather than a slice of a list.
 *
 * Smaller than the food rows' --radius-md, which would eat most of the height
 * of a 57px row. */
.setgrid .row-swipe { border-radius: var(--radius-sm); }

.setrow .idx {
  font-family: var(--font-mono);
  font-size: var(--text-sm);
  color: var(--color-text-dim);
  text-align: center;
}
.setrow .idx.warm { color: var(--color-carbs); }

.setrow .cell {
  width: 100%;
  min-width: 0;
  background: var(--color-surface-raised);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-pill);
  padding: var(--space-2);
  text-align: center;
  outline: none;
  /* Not --text-base: 15.2px is under the 16px iOS zoom threshold, and these are
     the most-tapped inputs in the app — every set typed was zooming Aana's
     viewport and leaving it panned. Same floor as .search input and base.css. */
  font-size: 16px;
  font-weight: 600;
  box-shadow: var(--shadow-sm);
}
.setrow .cell:focus { border-color: var(--color-primary); }

/* What this set was last time. Quiet on purpose — it is reference, not input,
   and it sits directly beside the box you are about to type into. */
.prev-cell {
  font-family: var(--font-mono);
  font-size: var(--text-xs);
  color: var(--color-text-dim);
  text-align: center;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

/* Dimmer than typed text, so "205" you have to beat never looks like "205" you
   already entered. */
.setrow .cell::placeholder {
  color: var(--color-text-dim);
  opacity: .75;
  font-weight: 400;
}

/* A workout folded away: one tappable line, holding its place at the top. */
.running-bar {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  width: 100%;
  padding: var(--space-3) var(--space-4);
  border: 1px solid var(--color-primary-soft);
  border-radius: var(--radius-md);
  background: var(--color-surface-raised);
  box-shadow: var(--shadow-inset);
  text-align: left;
}

.running-name {
  font-weight: 600;
  font-size: var(--text-sm);
  flex: 1;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.running-meta {
  font-family: var(--font-mono);
  font-size: var(--text-xs);
  color: var(--color-text-muted);
  flex: none;
}

/* Points the way the bar opens, and pairs with the ⌄ that folded it. */
.running-caret {
  flex: none;
  color: var(--color-primary);
  font-size: var(--text-lg);
  line-height: 1;
}

/* Said in words as well as shape: a bar that only looked tappable was a bar
   nobody could see how to get back into. */
.running-open {
  flex: none;
  font-family: var(--font-mono);
  font-size: var(--text-xs);
  letter-spacing: .1em;
  text-transform: uppercase;
  color: var(--color-primary);
}

/* Top-left, like the chevron that folds a panel away anywhere else. */
.workout-head {
  display: flex;
  align-items: center;
  gap: var(--space-2);
}

.workout-title {
  font-family: var(--font-mono);
  font-size: var(--text-xs);
  letter-spacing: .16em;
  text-transform: uppercase;
  color: var(--color-text-dim);
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* A finished set goes green edge to edge. Mid-set, from a few feet away and out
 * of breath, "which ones have I done" has to be answerable without reading —
 * and the tick alone was too small a signal to carry that.
 *
 * Two layers, not one: the green is deliberately a translucent tint, and the
 * remove action sits *behind* this row waiting to be swiped to. A see-through
 * background let the red show through every finished set. Painting the tint
 * over an opaque base keeps the tint honest and the row solid.
 *
 * Keeping consecutive green rows apart is .setgrid's gap, not the hairline —
 * a 1px border in --color-border does not survive being laid over this tint. */
.setrow.done {
  background:
    linear-gradient(var(--color-success-soft), var(--color-success-soft)),
    var(--color-surface-raised);
}

.setrow.done .cell { background: transparent; color: var(--color-text-muted); }

/* Unfinished sets get the same tick, greyed out. An empty circle read as a
   separate control rather than the off state of this one. */
.setrow .check {
  height: 38px;
  border: 1px solid var(--color-border);
  border-radius: var(--radius-pill);
  display: grid;
  place-items: center;
  color: var(--color-text-dim);
  font-size: var(--text-base);
  opacity: .45;
}

.setrow.done .check {
  background: var(--color-success);
  border-color: var(--color-success);
  color: var(--color-ink);
  opacity: 1;
}

/* Demonstration frames: the DB ships a start and end position per exercise. */
.shots {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
  gap: var(--space-2);
}

.shots img {
  width: 100%;
  aspect-ratio: 4 / 3;
  object-fit: cover;
  border-radius: var(--radius-md);
  background: var(--color-ink);
}

/* ---- the loaded bar -------------------------------------------------------- */
/* The calorie readout as a barbell: each plate's WIDTH is that macro's share of
   the day. Heights are equal on purpose — a taller plate reads as a heavier one
   and would contradict what the widths are saying. */

.rack {
  display: flex;
  align-items: center;
  height: 38px;
  position: relative;
  flex: none;
  overflow: hidden;
}

/* The shaft runs the full width; plates load from the left. */
.rack::before {
  content: "";
  position: absolute;
  left: 0; right: 0; top: 50%;
  height: 4px;
  margin-top: -2px;
  border-radius: var(--radius-pill);
  background: #3F4855;
}

.rack i {
  position: relative;
  display: block;
  height: 28px;
  border-radius: 4px;
  margin-right: 3px;
  box-shadow: inset 0 0 0 1px rgba(0, 0, 0, .3), 0 3px 8px rgba(0, 0, 0, .35);
}

/* Caps the loaded end, like a real collar. */
.rack .collar {
  width: 7px;
  height: 15px;
  border-radius: 2px;
  background: var(--color-text-dim);
  box-shadow: 0 2px 5px rgba(0, 0, 0, .4);
}

/* ---- what's left, in food you actually eat --------------------------------- */
/* "253 kcal" is abstract; "one chicken breast" is a decision. Built from your
   own frequents, so it isn't generic advice. */

.infood {
  font-family: var(--font-mono);
  font-size: var(--text-xs);
  letter-spacing: .04em;
  color: var(--color-carbs);
}

/* ---- plate maths ----------------------------------------------------------- */
/* Every lifter does this arithmetic between sets and occasionally gets it wrong. */

/* Plates for a weight that is still a suggestion. Dimmed like the placeholder
   digits beside them, and back to full ink the moment the weight is typed or
   the set checked. */
.loadout.is-ghost b { opacity: .5; }

.loadout {
  display: flex;
  align-items: center;
  gap: var(--space-1);
  flex-wrap: wrap;
  padding-top: var(--space-1);
}

/* On the row rather than under it: three of these stacked below the grid read as
   a separate list, and finding the one belonging to your set meant counting down
   it.
   
   Stacked, and only ever stacked. Laid out in a line they fitted one load and
   wrapped the next, so a card showed some rows across and some rows down — the
   arrangement changing with the weight rather than meaning anything. Down the
   column every load reads the same way, and the track only has to be as wide as
   the widest single chip. */
.setrow .loadout {
  padding-top: 0;
  flex-direction: column;
  flex-wrap: nowrap;
  align-items: flex-end;
  gap: 2px;
}

/* Nothing to load on a dumbbell or a machine, so that card gives the track back
   rather than holding a column of empty space down every row. Prev keeps its 40:
   the point of a fixed width is that the rows match from card to card. */
/* :not(.is-cardio) is load-bearing, not decoration. `:not(:has(...))` takes the
   specificity of its argument, so this selector outweighs the cardio rule below
   and was overriding it on the one card that rule exists for. */
.card:not(.is-cardio):not(:has(.setrow .loadout b)) .sethead,
.card:not(.is-cardio):not(:has(.setrow .loadout b)) .setrow {
  grid-template-columns: 22px 40px minmax(44px, 1fr) minmax(36px, 1fr) 0 38px;
}

/* Cardio is the exception, and the only one. Its Prev cell carries a whole line
   — "3.23 mi · 26:00 · 8:03 /mi" — rather than one number and a rep count, and
   there are no plates competing for the room. */
.card.is-cardio .sethead,
.card.is-cardio .setrow {
  /* Wider floors than the weight row's: a distance is "3.23", four characters
     and a point, where a weight is three digits. Giving Prev the slack without
     raising these clipped the distance box to "3.2". */
  grid-template-columns: 22px minmax(0, 2.4fr) minmax(56px, 1fr) minmax(48px, 1fr) 0 38px;
}

.loadout b {
  font-family: var(--font-mono);
  font-size: var(--text-xs);
  font-weight: 700;
  /* Tighter than the old stacked chips: two of them share the track now, and a
     mixed load only stays on one line if they do. */
  padding: 3px 4px;
  border-radius: 5px;
  color: var(--color-ink);
  box-shadow: var(--shadow-sm);
}

/* ---- personal record ------------------------------------------------------- */
/* You find out at the moment it happens, not in a stats screen weeks later. */

.setrow.record .check {
  background: var(--color-carbs);
  border-color: var(--color-carbs);
  color: var(--color-ink);
}

/* ============================================================================
   Desktop
   ============================================================================
   The phone layout is the real one — this is the same markup rearranged, not a
   second app. Three changes carry it: the tab bar becomes a top nav, content
   gets a readable measure instead of stretching edge to edge, and the log panel
   docks to the right so Today stays visible while you add to it. */

@media (min-width: 900px) {

  /* Order rather than a different DOM: the nav sits under the brand. */
  .app-bar   { order: 0; }
  .tabbar    { order: 1; }
  .app-main  { order: 2; }

  .app-bar {
    max-width: 1100px;
    width: 100%;
    margin: 0 auto;
    padding: var(--space-6) var(--space-6) var(--space-3);
  }

  .app-bar h1 { font-size: var(--text-xl); }

  .tabbar {
    position: static;
    display: flex;
    justify-content: center;
    gap: var(--space-2);
    max-width: 1100px;
    width: 100%;
    margin: 0 auto var(--space-4);
    padding: 0 var(--space-6) var(--space-3);
    border-top: 0;
    border-bottom: 1px solid var(--color-border);
    background: transparent;
  }

  .tabbar button {
    padding: var(--space-2) var(--space-4);
    border-radius: var(--radius-pill);
  }

  .tabbar button.is-active {
    background: var(--color-primary-soft);
    box-shadow: inset 0 0 0 1px var(--color-primary);
  }

  /* The indicator was a bottom-bar idiom; a pill reads better in a top nav. */
  .tabbar button.is-active::before { display: none; }

  /* A measure, not the full width of a monitor. */
  .app-main {
    max-width: 720px;
    width: 100%;
    margin: 0 auto;
    padding-left: var(--space-6);
    padding-right: var(--space-6);
  }

  /* The panel is fixed, so the content must be told to keep out of its way —
     otherwise a centred column is centred in the viewport and runs underneath. */
  .app.has-panel { padding-right: 440px; }

  /* Docked rather than covering: you can see Today while you add to it. */
  .log-panel {
    inset: 0 0 0 auto;
    width: 440px;
    max-width: 100%;
    margin: 0;
    border-left: 1px solid var(--color-border);
    box-shadow: var(--shadow-lg);
    padding: var(--space-6);
  }

  /* Sheets become centred dialogs — a bottom sheet on a monitor is a phone
     idiom stranded on the wrong device. */
  .sheet-backdrop { align-items: center; justify-content: center; }

  .sheet {
    max-width: 520px;
    border-radius: var(--radius-lg);
    border: 1px solid var(--color-border);
    box-shadow: var(--shadow-lg);
    max-height: 82%;
  }

  .grabber { display: none; }

  .fab-stack { right: var(--space-8); bottom: var(--space-8); }

  /* Pointer users get hover feedback that touch users never see. */
  .row:hover .row-text b { color: var(--color-primary-hover); }
  .row:hover { border-color: var(--color-primary-soft); }
  .card .row:hover { border-color: transparent; border-bottom-color: var(--color-border); }
  .tap:hover  { border-color: var(--color-primary); }
  .chip:hover { border-color: var(--color-primary); }
}

@media (min-width: 1400px) {
  .app-main { max-width: 820px; }
}

/* A sheet opened from inside another sheet. Equal z-index means DOM order
   decides, and the exercise history sits earlier in the document than the
   routine sheet it opens from — so it needs saying explicitly. */
.sheet-backdrop.is-nested { z-index: 30; }

/* ---- exercise detail ------------------------------------------------------- */
/* Figure and instructions side by side once there's room: the diagram says which
   muscle, the words say how. */

.detail-top {
  display: grid;
  gap: var(--space-3);
  grid-template-columns: 1fr;
}

@media (min-width: 560px) {
  .detail-top { grid-template-columns: 1.1fr 1fr; align-items: start; }
}

.how ol {
  margin: 0;
  padding-left: var(--space-4);
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
  font-size: var(--text-sm);
  color: var(--color-text-muted);
  max-height: 240px;
  overflow-y: auto;
  /* Nested two deep: without this, running out of instructions scrolled the
     sheet, and running out of sheet scrolled the page. */
  overscroll-behavior: contain;
}

.how li::marker {
  color: var(--color-text-dim);
  font-family: var(--font-mono);
  font-size: var(--text-xs);
}

/* The picker's filter rows. Their own stack with a tighter gap than the sheet's:
   three rows of chips at the sheet's 16px read as three separate sections, and
   they are one control. */
.picker-filters {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
  flex: none;
}

/* ---- routine categories ---------------------------------------------------- */

.routine-group {
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
}

/* The whole bar is the control, so the target is the row rather than the caret —
   a 12px chevron is not something to ask a thumb to hit. */
.group-head {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  width: 100%;
  padding: var(--space-2) 0;
  min-height: 44px;
  text-align: left;
  font-family: var(--font-mono);
  font-size: var(--text-xs);
  letter-spacing: .12em;
  text-transform: uppercase;
  color: var(--color-text-muted);
  cursor: pointer;
}

.group-head b { color: var(--color-text); font-weight: 600; }
.group-head small { margin-left: auto; color: var(--color-text-dim); }

.group-caret {
  display: grid;
  place-items: center;
  width: 14px;
  height: 14px;
  flex: none;
  color: var(--color-text-dim);
  /* Points down when open, right when shut. Rotating one glyph rather than
     swapping two keeps it optically centred, which a bare chevron never was. */
  transform: rotate(90deg);
  transition: transform .15s ease;
}
.group-caret.is-shut { transform: rotate(0deg); }
.group-caret svg { width: 100%; height: 100%; display: block; }

/* ---- routine cards --------------------------------------------------------- */
/* A name and a count left the row mostly empty. These answer what you'd ask
   before tapping: what does it hit, when did I last do it, what does it cost. */

.routine-grid {
  display: grid;
  grid-template-columns: 1fr;
  gap: var(--space-3);
}

@media (min-width: 700px) {
  .routine-grid { grid-template-columns: repeat(auto-fill, minmax(240px, 1fr)); }
}

.routine-card {
  background: var(--color-surface);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-md);
  padding: var(--space-3) var(--space-4);
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
  text-align: left;
  box-shadow: var(--shadow-inset), var(--shadow-md);
  transition: border-color var(--transition-fast);
}

.routine-card:hover { border-color: var(--color-primary); }

.routine-top {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: var(--space-3);
}

.routine-name {
  font-size: var(--text-base);
  font-weight: 700;
  letter-spacing: -.01em;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.routine-meta {
  font-family: var(--font-mono);
  font-size: var(--text-xs);
  color: var(--color-text-muted);
  letter-spacing: .05em;
  margin-top: 2px;
}

/* Read the shape before the name: blue is a pull day, red a push day. */
.coverage { display: flex; align-items: center; gap: 5px; }

.coverage i {
  height: 7px;
  border-radius: var(--radius-pill);
  display: block;
  min-width: 6px;
}

.coverage span {
  font-family: var(--font-mono);
  font-size: var(--text-xs);
  letter-spacing: .09em;
  text-transform: uppercase;
  color: var(--color-text-dim);
  margin-left: 2px;
  flex: none;
}

.routine-stats {
  display: flex;
  gap: var(--space-4);
  font-family: var(--font-mono);
  font-size: var(--text-xs);
  color: var(--color-text-muted);
  letter-spacing: .04em;
}

.routine-stats b { color: var(--color-text); font-weight: 700; }

/* The sheet follows the finger, so it must not fight the drag with its own
   transition mid-gesture — that is set and cleared in JS. */
.sheet { will-change: transform; touch-action: pan-y; }

.grabber { cursor: grab; }
.grabber:active { cursor: grabbing; }

/* ---- stats ----------------------------------------------------------------- */
/* Single-series charts throughout: the card title names the series, so no legend
   is needed and no two hues ever sit adjacent — which matters here, because the
   plate red and green are 5.1 ΔE apart under deuteranopia. */

.stat-hero {
  font-family: var(--font-mono);
  font-variant-numeric: tabular-nums;
  font-size: var(--text-2xl);
  font-weight: 700;
  letter-spacing: -.035em;
  line-height: 1.1;
}

.stat-hero small {
  font-size: var(--text-sm);
  font-weight: 400;
  color: var(--color-text-muted);
  margin-left: 4px;
}

.stat-sub {
  font-family: var(--font-mono);
  font-size: var(--text-xs);
  color: var(--color-text-muted);
  letter-spacing: .04em;
}

/* ---- interactive plot ------------------------------------------------------ */
/* Every chart on this page used to be an SVG string handed to x-html, which
   cannot carry a pointer layer — a picture rather than something you can
   interrogate. These render real elements, so a crosshair and a readout can
   hang off them. */

.plot {
  position: relative;
  touch-action: pan-y;      /* tracking the finger must not block scrolling */
  cursor: pointer;          /* it is tapped, not hovered */
  /* A tap selects a point, so the browser must not also select the text of the
     readout underneath when you drag along the series. */
  -webkit-user-select: none;
  user-select: none;
}

.plot svg {
  display: block;
  width: 100%;
  height: auto;             /* aspect preserved: a stretched plot skews the slope */
  overflow: visible;
}

/* non-scaling-stroke throughout, or "2px" means two viewBox units and the line
   thickens with the card. */
.plot-line {
  fill: none;
  stroke: var(--color-primary-hover);
  stroke-width: 2;
  stroke-linecap: round;
  stroke-linejoin: round;
  vector-effect: non-scaling-stroke;
}

/* A tint, not a mass. The fill's floor is the axis minimum rather than zero, so
   its height encodes nothing — and at full strength it outweighed the 2px stroke
   that carries the actual signal. */
.plot-area { fill: var(--color-primary); opacity: .1; stroke: none; }

/* Context, not data, so it stays under the line's weight. */
.plot-target {
  stroke: var(--color-success);
  stroke-width: 1;
  stroke-dasharray: 3 3;
  opacity: .5;
  vector-effect: non-scaling-stroke;
}

.plot-crosshair {
  stroke: var(--color-text-dim);
  stroke-width: 1;
  vector-effect: non-scaling-stroke;
}

.plot-dot {
  fill: var(--color-primary-hover);
  /* A ring in the surface colour, so a dot on the line still reads as a dot. */
  stroke: var(--color-surface-raised);
  stroke-width: 1.5;
  vector-effect: non-scaling-stroke;
}

.plot-dot.is-on {
  fill: var(--color-text);
  r: 2.6;
}

/* Value leads, label follows — the reader has the date and wants the number. */
/* The duration row on the finish sheet: two small number boxes, their units,
   and the last-set chip, all on one line. The boxes are sized to their content
   rather than stretched — a duration is at most three digits, and a full-width
   input reads as a text field to fill in. */
.finish-when {
  display: flex;
  align-items: center;
  gap: var(--space-2);
}

.finish-when input[type="number"] {
  width: 4.2rem;
  flex: none;
  text-align: center;
}

.finish-when > span {
  font-family: var(--font-mono);
  font-size: var(--text-sm);
  color: var(--color-text-muted);
}

/* ---- post-workout ---------------------------------------------------------- */

.celebrate { text-align: center; }

.celebrate-mark {
  width: 56px;
  height: 56px;
  margin: var(--space-2) auto 0;
}
.celebrate-mark svg { width: 100%; height: 100%; display: block; }

.celebrate-head {
  font-family: var(--font-mono);
  font-size: var(--text-2xl);
  letter-spacing: -.02em;
  margin: var(--space-3) 0 0;
}

.celebrate-note {
  font-size: var(--text-sm);
  color: var(--color-text-muted);
  margin: var(--space-2) 0 var(--space-2);
}

/* Centred here, where the grid is a summary rather than a table to scan. */
.celebrate .stat-cell { align-items: center; }

/* The day grid. Squares are laid out in user units and scaled to the card, so
   they stay square whatever the width is. */
.grid-plot {
  display: block;
  width: 100%;
  height: auto;
  overflow: visible;
}

/* ---- muscle balance radar ------------------------------------------------- */

/* Square, so the shape is not skewed by the card's width — a radar read on a
   stretched canvas says something untrue about which spoke is longest. */
.plot-radar svg { aspect-ratio: 1; height: auto; }

/* Grid and spokes are scaffolding: present enough to read a magnitude against,
   quiet enough that the shape is what you see. */
.radar-ring,
.radar-spoke {
  fill: none;
  stroke: var(--color-border);
  stroke-width: 1;
  vector-effect: non-scaling-stroke;
}

.radar-even {
  fill: none;
  stroke: var(--color-text-dim);
  stroke-width: 1;
  stroke-dasharray: 3 3;
  vector-effect: non-scaling-stroke;
  opacity: .55;
}

.radar-shape {
  fill: var(--color-primary-soft);
  stroke: var(--color-primary-hover);   /* validated 3:1 against the card */
  stroke-width: 2;
  stroke-linejoin: round;
  vector-effect: non-scaling-stroke;
}

.radar-dot {
  fill: var(--color-primary-hover);
  stroke: var(--color-surface-raised);  /* the 2px surface ring on an overlap */
  stroke-width: 2;
  vector-effect: non-scaling-stroke;
}

.radar-dot.is-on {
  fill: var(--color-text);
  r: 2.4;
}

/* Text tokens, never the series colour.
 *
 * font-size is in viewBox units, so it scales with the card: 5px here rendered
 * near 19px on a phone and shouldered "Shoulders" off the edge. 3.2 lands around
 * 11px at a phone's width, which is this page's --text-xs. text-anchor comes
 * from the plot, per point. */
.radar-label {
  fill: var(--color-text-muted);
  font-family: var(--font-mono);
  font-size: 3.2px;
  letter-spacing: .06em;
  text-transform: uppercase;
  dominant-baseline: middle;
}

.plot-readout {
  display: flex;
  align-items: baseline;
  gap: var(--space-2);
  margin-top: var(--space-2);
  font-family: var(--font-mono);
}

.plot-readout b { font-size: var(--text-lg); font-variant-numeric: tabular-nums; }

.plot-readout span {
  font-size: var(--text-xs);
  color: var(--color-text-muted);
}

.plot-note {
  font-size: var(--text-xs);
  color: var(--color-text-dim);
  margin: 0;
}

/* ---- stat cells ------------------------------------------------------------ */

/* A row that opens something. .row already sets width and text-align, so this
   only has to undo the button defaults and mark it as tappable. */
.row-tap {
  font: inherit;
  color: inherit;
  cursor: pointer;
}

.row-tap[aria-expanded="true"] {
  border-color: var(--color-primary-hover);
  border-bottom-left-radius: 0;
  border-bottom-right-radius: 0;
}

/* Hangs off the bottom of its row rather than floating apart from it. */
.lift-detail {
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
  padding: var(--space-3) var(--space-4) var(--space-4);
  background: var(--color-surface);
  border: 1px solid var(--color-primary-hover);
  border-top: 0;
  border-radius: 0 0 var(--radius-md) var(--radius-md);
}

.plot-mini {
  display: block;
  width: 100%;
  height: auto;
  overflow: visible;
}

.stat-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(104px, 1fr));
  gap: var(--space-2);
}

.stat-cell {
  display: flex;
  flex-direction: column;
  gap: 2px;
  padding: var(--space-2) var(--space-3);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-sm);
  background: var(--color-surface);
  min-width: 0;
}

.stat-cell small {
  font-family: var(--font-mono);
  font-size: var(--text-xs);
  letter-spacing: .08em;
  text-transform: uppercase;
  color: var(--color-text-dim);
}

.stat-cell b {
  font-family: var(--font-mono);
  font-variant-numeric: tabular-nums;
  font-size: var(--text-base);
}

.stat-cell span {
  font-family: var(--font-mono);
  font-size: var(--text-xs);
  color: var(--color-text-muted);
}

/* Direction is carried by the sign as well as the colour — never colour alone. */
.delta {
  font-family: var(--font-mono);
  font-variant-numeric: tabular-nums;
  font-size: var(--text-sm);
  color: var(--color-text-dim);
  flex: none;
  min-width: 44px;
  text-align: right;
}

.delta.is-down { color: var(--color-success); }
.delta.is-up   { color: var(--color-warning); }

/* Tap-to-focus confirmation. Purely feedback — the camera does the work. */
.focus-ping {
  position: absolute;
  width: 64px;
  height: 64px;
  margin: -32px 0 0 -32px;
  border: 2px solid var(--color-plate-white);
  border-radius: var(--radius-sm);
  pointer-events: none;
  animation: focus-ping 700ms ease-out forwards;
}

@keyframes focus-ping {
  0%   { opacity: 0; transform: scale(1.4); }
  25%  { opacity: 1; transform: scale(1); }
  100% { opacity: 0; transform: scale(1); }
}

@media (prefers-reduced-motion: reduce) {
  .focus-ping { animation: none; opacity: 1; }
}

/* Serving-size converter in the review form. */
.serving-fix {
  display: flex;
  align-items: flex-end;
  gap: var(--space-2);
  margin-top: var(--space-2);
}

.serving-fix .field { flex: 1; margin: 0; }
.serving-fix .btn   { flex: none; }

/* What the source actually returned for its serving fields. Diagnostic. */
.servingraw {
  font-family: var(--font-mono);
  font-size: 11px;
  line-height: 1.5;
  color: var(--color-text-dim);
  word-break: break-word;
  margin: 0;
}

/* Scanner controls: zoom is the one that matters, so it gets the width. */
.scan-controls {
  display: flex;
  align-items: center;
  gap: var(--space-3);
}

.scan-zoom {
  flex: 1;
  display: flex;
  align-items: center;
  gap: var(--space-2);
  font-family: var(--font-mono);
  font-size: var(--text-xs);
  color: var(--color-text-muted);
}

.scan-zoom input[type="range"] {
  flex: 1;
  accent-color: var(--color-primary);
  min-height: 44px;              /* same thumb target as everything else */
}

.scan-zoom span { min-width: 3ch; text-align: right; font-variant-numeric: tabular-nums; }

/* The progress photo beside a selected weight reading. align-self centres it
   against the baseline-aligned text without disturbing that alignment. */
.plot-photo {
  align-self: center;
  margin-left: auto;
  width: 36px;
  height: 36px;
  padding: 0;
  border: 1px solid var(--color-border);
  border-radius: var(--radius-sm);
  overflow: hidden;
  background: var(--color-surface-raised);
  cursor: pointer;
  flex: none;
}
.plot-photo img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
}

/* The paste hint exists only where a clipboard is likelier than a camera —
   `pointer: fine` is a mouse or trackpad, which is the desktop the feature
   was built for. Phones never see it. */
.paste-hint { display: none; }
@media (pointer: fine) {
  .paste-hint {
    display: block;
    margin: 0;
    font-family: var(--font-mono);
    font-size: var(--text-xs);
    letter-spacing: .04em;
    color: var(--color-text-dim);
  }
}

/* ---- complete-day marking --------------------------------------------------- */

/* The look-back question at the top of the logger. */
.review-ask {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-3);
  padding: var(--space-3) var(--space-4);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-md);
  background: var(--color-surface);
  font-size: var(--text-sm);
}
.review-ask-buttons { display: flex; gap: var(--space-2); flex: none; }
.review-ask .btn { min-height: 38px; padding: var(--space-2) var(--space-4); }

/* The day's verdict on the date bar. */
.day-mark { color: var(--color-text-dim); }
.day-mark.is-complete { color: var(--color-success); border-color: var(--color-success); }
.day-mark.is-incomplete { color: var(--color-danger); text-decoration: line-through; }
