/* LOGBOOK — dark, dense, gym-legible. One accent, hairlines instead of boxes. */

:root {
  --bg: #0b0c0e;
  --surface: #131519;
  --surface-2: #191c21;
  --line: #23262c;
  --line-soft: #1a1d22;
  --ink: #eef0f2;
  --ink-2: #a8aeb7;
  --ink-3: #8c939d;
  /* The accent is per person: app.js overrides --acid-rgb on <html> at boot and every
     other accent token derives from it, so one line recolours the whole app. */
  --acid-rgb: 200, 255, 46;
  --acid: rgb(var(--acid-rgb));
  --acid-dim: rgba(var(--acid-rgb), 0.55);
  --up: var(--acid);
  --down: #ff6a5c;
  --warn: #ffc247;
  --r: 12px;
  --r-s: 8px;
  --tap: 48px;
  --pad: 18px;
  --display: 'Inter Tight', system-ui, -apple-system, 'Segoe UI', sans-serif;
  --mono: 'Roboto Mono', ui-monospace, 'SF Mono', Menlo, monospace;
  --safe-b: env(safe-area-inset-bottom, 0px);
  --safe-t: env(safe-area-inset-top, 0px);   /* iPhone status bar / notch when installed to home screen */
  --top-h: calc(55px + var(--safe-t));   /* sticky topbar */
  --tab-h: 59px;   /* fixed tab bar, above the safe area */
}

* { box-sizing: border-box; -webkit-tap-highlight-color: transparent; }

html {
  background: var(--bg);
  /* The tab bar, the session bar and the rest timer all float over the bottom of the
     page. Without this the browser scrolls a focused set row flush to the viewport
     bottom — under all three — the moment you tap into it. 147px = the session bar
     (59) + the rest timer (76) + 12 of air; it has to clear the timer, not just reach
     it, or the bottom of the input you were sent to sits under the clock.
     ponytail: the 76 is the timer at one line of label — a wrapped label eats the air. */
  scroll-padding-bottom: calc(var(--tab-h) + 147px + var(--safe-b));
}

body {
  margin: 0;
  background: var(--bg);
  color: var(--ink);
  font-family: var(--display);
  font-size: 16px;
  line-height: 1.45;
  -webkit-font-smoothing: antialiased;
  overscroll-behavior-y: contain;
}

h1, h2, h3 { font-weight: 800; letter-spacing: -0.03em; margin: 0; }
button, input, select, textarea { font: inherit; color: inherit; }
button { cursor: pointer; border: 0; background: none; }
a { color: var(--acid); }

.mono { font-family: var(--mono); font-variant-numeric: tabular-nums; }
.hide { display: none !important; }

/* ---------------------------------------------------------------- shell */

.shell { max-width: 760px; margin: 0 auto; padding: 0 var(--pad) calc(132px + var(--safe-b)); }

.topbar {
  position: sticky; top: 0; z-index: 40;
  display: flex; align-items: center; gap: 12px;
  padding: calc(14px + var(--safe-t)) var(--pad) 12px;
  max-width: 760px; margin: 0 auto;
  background: var(--bg);
  border-bottom: 1px solid var(--line-soft);
}
.topbar #phaseChip { min-width: 0; overflow: hidden; text-overflow: ellipsis; }
.topbar #whoChip { flex: none; }
@media (max-width: 460px) { .topbar #phaseChip { display: none; } }
.wordmark {
  font-size: 15px; font-weight: 800; letter-spacing: 0.22em;
  text-transform: uppercase;
}
.wordmark b { color: var(--acid); font-weight: 800; }
.topbar .spacer { flex: 1; }

.chip {
  font-family: var(--mono); font-size: 11px; letter-spacing: 0.08em;
  text-transform: uppercase; color: var(--ink-2);
  border: 1px solid var(--line); border-radius: 100px; padding: 5px 11px;
  white-space: nowrap;
}
.chip.acid { color: var(--acid); border-color: var(--acid-dim); }
.chip.warn { color: var(--warn); border-color: #5a4a1c; }
.chip.solid { background: var(--acid); color: #10130a; border-color: var(--acid); font-weight: 700; }

/* ------------------------------------------------------------------ nav */

.tabs {
  position: fixed; left: 0; right: 0; bottom: 0; z-index: 50;
  display: grid; grid-auto-flow: column; grid-auto-columns: 1fr;
  background: rgba(11, 12, 14, 0.92);
  backdrop-filter: blur(14px);
  border-top: 1px solid var(--line);
  padding-bottom: var(--safe-b);
}
.tabs button {
  min-height: var(--tap); padding: 9px 4px 8px;
  display: flex; flex-direction: column; align-items: center; gap: 4px;
  font-size: 11px; font-weight: 700; letter-spacing: 0.08em; text-transform: uppercase;
  color: var(--ink-3);
}
.tabs button svg { width: 21px; height: 21px; stroke: currentColor; fill: none; stroke-width: 1.9; }
.tabs button[aria-selected='true'] { color: var(--acid); }
.tabs button .dot {
  position: absolute; margin-left: 22px; margin-top: -2px;
  width: 6px; height: 6px; border-radius: 50%; background: var(--down);
}
@media (min-width: 800px) {
  .tabs {
    max-width: 760px; margin: 0 auto;
    border: 1px solid var(--line); border-bottom: 0; border-radius: var(--r) var(--r) 0 0;
  }
}

/* --------------------------------------------------------------- pieces */

.eyebrow {
  font-family: var(--mono); font-size: 11px; letter-spacing: 0.14em;
  text-transform: uppercase; color: var(--ink-3); margin: 26px 0 10px;
}
.eyebrow:first-child { margin-top: 8px; }

.creed {
  font-family: var(--display); font-size: 13.5px; text-transform: none;
  letter-spacing: 0; color: var(--ink-2); margin: 26px 0 10px;
}

.card {
  background: var(--surface); border: 1px solid var(--line);
  border-radius: var(--r); padding: 16px;
}
.card + .card { margin-top: 10px; }
/* A filled box means "you touch this". Read-only panels get a hairline instead —
   otherwise every screen is the same grey rectangle and the border says nothing. */
.card.plain {
  background: none; border: 0; border-top: 1px solid var(--line);
  border-radius: 0; padding: 16px 0;
}
.card.plain + .card.plain { margin-top: 0; }

.hero {
  border: 1px solid var(--line); border-radius: var(--r);
  background:
    radial-gradient(120% 140% at 100% 0%, rgba(var(--acid-rgb), 0.09), transparent 55%),
    var(--surface);
  padding: 20px; position: relative; overflow: hidden;
}
.hero .day {
  font-family: var(--mono); font-size: 11px; letter-spacing: 0.14em;
  text-transform: uppercase; color: var(--acid);
}
.hero h2 { font-size: clamp(30px, 9vw, 42px); line-height: 1; margin: 8px 0 6px; }
.hero p { color: var(--ink-2); margin: 0; font-size: 14px; text-wrap: pretty; }
.hero .meta { display: flex; gap: 8px; flex-wrap: wrap; margin-top: 14px; }

.btn {
  display: flex; align-items: center; justify-content: center; gap: 8px;
  min-height: var(--tap); width: 100%; border-radius: var(--r-s);
  background: var(--acid); color: #10130a; font-weight: 800; letter-spacing: -0.01em;
  font-size: 16px; margin-top: 16px; text-decoration: none;
}
.btn:active { transform: translateY(1px); }
.btn.ghost { background: none; border: 1px solid var(--line); color: var(--ink); }
.btn.slim { min-height: 40px; font-size: 14px; margin-top: 10px; }
/* The two seconds after a copy button did what it says. The accent is the app's one
   "that worked", and it is on the button itself so there is no doubt which one. */
.btn.ghost.ok { color: var(--acid); border-color: var(--acid-dim); }
.btn[disabled] { opacity: 0.45; }

/* --------------------------------------------------------- check-in row */

.checkin .row {
  display: flex; align-items: stretch; gap: 10px;
  padding: 13px 0; border-top: 1px solid var(--line-soft);
}
.checkin .row:first-child { border-top: 0; }
.checkin .row > label {
  font-family: var(--mono); font-size: 11px; letter-spacing: 0.1em;
  text-transform: uppercase; color: var(--ink-3); width: 76px; flex: none;
  align-self: center; white-space: nowrap;   /* 76 fits "Muay Thai" on one line */
}
.opts {
  display: grid; grid-auto-flow: column; grid-auto-columns: 1fr;
  gap: 6px; flex: 1; min-width: 0;
}
.opts button {
  min-width: 0; min-height: 40px; padding: 0 6px;
  border: 1px solid var(--line); border-radius: var(--r-s);
  font-size: 12.5px; line-height: 1.2; font-weight: 600; color: var(--ink-2);
  background: var(--surface-2);
}
.opts button[aria-pressed='true'] {
  background: var(--acid); border-color: var(--acid); color: #10130a; font-weight: 800;
}
/* A lone yes-tick takes one cell of a pair, like Cardio's Yes — full width it read as the CTA. */
.opts.half { grid-template-columns: 1fr 1fr; }
/* The plan switch is a setting, not an action: tinted, so the solid fill stays the CTA's. */
.opts button[data-plan][aria-pressed='true'] {
  background: rgba(var(--acid-rgb), 0.14); border-color: var(--acid); color: var(--acid);
}
/* who() prints a name in that person's colour; on a filled button it has to give way. */
.opts button[aria-pressed='true'] b { color: #10130a !important; }
/* At phone width a 56px label column squeezes the 4-option rows until the longest
   word ("Stressed") clips. Stack the label instead — same as the bodyweight block. */
@media (max-width: 460px) {
  .checkin .row { display: block; }
  .checkin .row > label { width: auto; display: block; margin-bottom: 8px; }
}

/* A run: distance, minutes, add. Two short boxes rather than one wide one — both are two
   or three characters, and a full-width input invites a sentence. */
/* flex:1 like .opts, its sibling rows: a row that stops two thirds of the way across
   while every row above it reaches the edge reads as an unfinished row, not a short one.
   Add goes to the far end for the same reason — the fill has to be visible. */
.runadd { display: flex; gap: 8px; align-items: center; flex-wrap: wrap; flex: 1; min-width: 0; }
/* Basis 64, capped at 84: a flex line wraps on the items' BASIS, before it shrinks any of
   them, so a basis of 84 put Add on a line of its own at 390px the moment the two boxes
   grew a unit each. The cap keeps them the same two short boxes they have always been. */
.runadd input {
  flex: 1 1 64px; max-width: 84px; min-width: 64px; min-height: var(--tap);
  background: var(--surface-2); border: 1px solid var(--line); border-radius: var(--r-s);
  padding: 6px 10px; text-align: center; font-size: 15px;
}
.runadd input:focus { outline: none; border-color: var(--acid); }
/* No placeholder in these two boxes: "—" is what a table cell says for NO DATA, so a run
   row pre-filled with it reads as a value somebody already entered. The units sit outside
   the box, so an empty box is not ambiguous. */
/* width:auto beats .btn's width:100%, or Add drops onto a line of its own under two
   three-character boxes and reads like a separate thing. */
.runadd .btn.slim { margin-top: 0; flex: 0 0 auto; width: auto; min-height: var(--tap); padding: 0 20px; margin-left: auto; }
/* The unit belongs to the box, not to the ghost inside it: a placeholder that says "km"
   is gone the moment you answer it. Same suffix the bodyweight row has always used. */
.runadd .unit { font-family: var(--mono); color: var(--ink-3); font-size: 14px; }
/* The list of today's runs belongs to the row above it, so it carries no hairline of its
   own — and it starts where the inputs start, not in their label's gutter. 86 = the 76px
   label column plus the row's 10px gap. */
.checkin .row.runlist { border-top: 0; padding-top: 0; display: block; }
/* Only while there IS a label column to clear — below 461 the label stacks above its row. */
@media (min-width: 461px) { .checkin .row.runlist, .checkin .row.bw > p { padding-left: 86px; } }
/* Bodyweight sits in the same label column as every other row; its helper line wraps under the input. */
.checkin .row.bw { flex-wrap: wrap; }
.checkin .row.bw > p { flex-basis: 100%; }
.checkin .row.runlist .list .item { padding: 8px 0; }
/* A single × is three pixels wide; the thing you have to hit is not. */
.checkin .row.runlist .list .item button.chip { min-width: 44px; }
/* The widget/health card. Its buttons are full 44px, not the 40px slim button the rest of
   Setup uses: these get tapped once, on somebody else's phone, by somebody holding it at
   arm's length — and a mis-tap on Regenerate costs a re-paste on two apps. */
.card.linkcard .btn.slim { min-height: 44px; }
#copyFallback {
  width: 100%; margin-top: 12px; padding: 10px;
  background: var(--surface-2); border: 1px solid var(--line); border-radius: var(--r-s);
  font-family: var(--mono); font-size: 11.5px; line-height: 1.5; color: var(--ink-2); resize: vertical;
}

.weigh { display: flex; align-items: baseline; gap: 8px; }
.weigh input {
  font-family: var(--mono); font-size: 40px; font-weight: 600; letter-spacing: -0.02em;
  width: auto; flex: 0 1 auto; min-width: 5ch; background: none; border: 0; border-bottom: 2px solid var(--line);
  padding: 4px 0 6px; color: var(--ink);
}
.weigh input:focus { outline: none; border-bottom-color: var(--acid); }
.weigh input::placeholder { color: #4a505a; }
.weigh .unit { font-family: var(--mono); color: var(--ink-3); font-size: 14px; }

.photorow { display: grid; grid-template-columns: repeat(3, 1fr); gap: 8px; margin-top: 12px; }
.photobtn {
  aspect-ratio: 3 / 4; border: 1px dashed var(--line); border-radius: var(--r-s);
  display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 6px;
  color: var(--ink-3); font-size: 11px; letter-spacing: 0.1em; text-transform: uppercase;
  font-family: var(--mono); overflow: hidden; position: relative; background: var(--surface-2);
}
/* The "+" is decoration, like .photowall .empty's label — as a text node it was the only
   bare "+" in the document and it read out as part of the button's name. */
/* The plus is decoration — the button already says "add your front picture". The alt-text
   form keeps it out of the accessibility tree, where three bare "+" read as content.
   The plain declaration first is the fallback for engines without the `/ alt` syntax. */
.photobtn:not(.done)::before { content: '+'; content: '+' / ''; font-size: 22px; line-height: 1; }
.photobtn img { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; }
.photobtn.done { border-style: solid; border-color: var(--acid-dim); color: var(--acid); }
.photobtn .tag {
  position: absolute; bottom: 0; left: 0; right: 0; padding: 4px 6px;
  background: rgba(11, 12, 14, 0.8); font-size: 11px; color: var(--ink-2);
}

/* -------------------------------------------------------------- workout */

.exlist { display: flex; flex-direction: column; gap: 10px; }

/* clip, not hidden: `overflow: hidden` makes the card a scroll container, and a
   sticky .ex-head inside one never sticks. `clip` corners the same and doesn't. */
.ex {
  border: 1px solid var(--line); border-radius: var(--r);
  background: var(--surface); overflow: clip;
}
.ex.done { border-color: var(--acid-dim); background: linear-gradient(var(--surface), rgba(var(--acid-rgb), 0.03)); opacity: 0.62; }
.ex.done .ex-head { cursor: pointer; }
.done-sum { font-size: 12px; color: var(--ink); white-space: nowrap; letter-spacing: 0.02em; }
.ex.done .ex-body { display: none; }
.ex.done .ex-head::after { content: '✓'; color: var(--acid); font-size: 14px; }
.ex.done.open { opacity: 1; }
.ex.done.open .ex-body { display: block; }
.ex.stretch { background: var(--surface-2); }

/* Sticky under the topbar (--top-h) so you can still see which lift you are in
   while you scroll its sets. Card background, so nothing reads through it. */
.ex-head {
  display: flex; align-items: center; gap: 10px; padding: 13px 14px;
  position: sticky; top: var(--top-h); z-index: 2; background: var(--surface);
}
.ex-num {
  font-family: var(--mono); font-size: 11px; color: var(--ink-3);
  width: 20px; flex: none;
}
.ex-name { font-weight: 700; font-size: 16px; letter-spacing: -0.01em; flex: 1; }
.ex-name .swapped { color: var(--acid); font-size: 11px; font-family: var(--mono); display: block; }
.ex-head .chip { font-size: 11px; padding: 3px 8px; }
.ex-head button.chip, .stretchrow button.chip {
  min-height: 44px; padding: 8px 12px; font-size: 11px;
  color: var(--ink); border-color: var(--line); position: relative;
}
.ex-head button.chip::after, .stretchrow button.chip::after {
  content: ''; position: absolute; inset: -7px;
}
.ex-head span.chip { border: 0; padding-left: 0; color: var(--ink-3); }

.ex-note {
  padding: 0 14px 10px 44px; color: var(--ink-2); font-size: 12.5px; line-height: 1.35;
}
.ex-body { padding: 0 14px 12px 14px; }

/* minmax(0,…): an <input> carries an intrinsic min-width from its `size`, and a 1fr
   track won't shrink past it — the boxes would push the target column off the card. */
.setrow {
  display: grid; grid-template-columns: 26px minmax(0, 1fr) minmax(0, 1fr) 80px;
  gap: 8px; align-items: center; padding: 6px 0;
}
.setrow .lbl { font-family: var(--mono); font-size: 11px; color: var(--ink-3); }
.setrow input {
  min-height: 52px; width: 100%; min-width: 0; text-align: center;
  font-family: var(--mono); font-size: 20px; font-weight: 600; font-variant-numeric: tabular-nums;
  background: var(--surface-2); border: 1px solid var(--line); border-radius: var(--r-s);
  color: var(--ink);
}
.setrow input:focus { outline: none; border-color: var(--acid); background: #1c2016; }
.setrow input::placeholder { color: #5a626c; font-weight: 400; font-size: 14px; font-style: italic; }
.setrow .target {
  font-family: var(--mono); font-size: 12px; color: var(--ink-3); text-align: right;
  line-height: 1.15; white-space: nowrap;
}
/* Tapping the target types last time's numbers into the row. */
.setrow button.target { padding: 0; min-height: 52px; }
.setrow button.target:active { color: var(--acid); }
.setrow .target b {
  display: block; font-size: 17px; font-weight: 700; color: var(--ink); letter-spacing: -0.02em;
}
.setrow .target .last { color: var(--ink-3); }
.setrow .target .mark { display: block; font-weight: 600; }
.setrow .target .mark.beat { color: var(--acid); }
.setrow .target .mark.under { color: var(--down); }
.setrow.beat { box-shadow: inset 3px 0 0 var(--acid); border-radius: 3px; }
.setrow.beat input { border-color: var(--line); }
.setrow.head {
  padding: 0 0 2px; font-family: var(--mono); font-size: 11px;
  color: var(--ink-3); letter-spacing: 0.08em;
}
.setrow.head span { text-align: center; }
.setrow.head span:last-child { text-align: right; }
/* On a wide screen a full-width box to type "110" into is absurd. Cap the fields. */
@media (min-width: 700px) {
  .setrow { grid-template-columns: 26px 170px 170px minmax(84px, 200px); justify-content: start; }
}
.setrow .verdict { font-size: 15px; text-align: right; }

.lastline {
  display: flex; align-items: center; gap: 8px; padding: 8px 0 2px;
  font-family: var(--mono); font-size: 11.5px; color: var(--ink-3);
  border-top: 1px dashed var(--line-soft); margin-top: 6px;
}
.lastline b { color: var(--ink-2); font-weight: 500; }
.lastline .cue { color: var(--acid); margin-left: auto; text-align: right; }

.setrow.rp-load { grid-template-columns: 46px 1fr 84px; }
.mini { display: flex; gap: 8px; margin-top: 4px; }
.mini.head {
  margin-top: 10px; font-family: var(--mono); font-size: 11px;
  color: var(--ink-3); letter-spacing: 0.08em;
}
.mini.head span { flex: 1; min-width: 0; text-align: left; }
/* min-width:0 is load-bearing: an <input> has an intrinsic min width from its `size`,
   and flex won't shrink past it — without this the 2nd and 3rd rest-pause rep boxes
   render off the card and are clipped away by .ex { overflow: hidden }. */
.mini input { flex: 1; min-width: 0; min-height: 44px; text-align: center; font-family: var(--mono);
  background: var(--surface-2); border: 1px solid var(--line); border-radius: var(--r-s); }
.mini input:focus { outline: none; border-color: var(--acid); }

.stretchrow { display: flex; align-items: center; gap: 10px; padding: 11px 14px; }
.stretchrow .ex-name { font-weight: 600; font-size: 14px; color: var(--ink-2); }
.tick {
  width: 44px; min-height: 44px; flex: none; border-radius: var(--r-s);
  border: 1px solid var(--line); background: var(--surface-2);
  display: grid; place-items: center; color: var(--ink-3);
  position: relative;
}
/* 34px visual, 48px hit area — sweaty hands, one hand on the rack */
.tick::after { content: ''; position: absolute; inset: -7px; }
.tick[aria-pressed='true'] { background: var(--acid); border-color: var(--acid); color: #10130a; }

/* Which day is being logged, and the way out of the printed session. A quiet hairline
   row under the hero, not part of it — the hero is the session, this is navigation.
   Same 44px tap target as the set rows, and both controls at the chip's mono size. */
.daybar {
  display: flex; align-items: center; gap: 10px; flex-wrap: wrap;
  border-top: 1px solid var(--line); margin-top: 12px; padding: 4px 0;
}
.daybar input[type='date'] {
  min-height: 44px; padding: 8px 10px; min-width: 0; flex: 0 1 auto;
  background: var(--surface-2); border: 1px solid var(--line); border-radius: 100px;
  color: var(--ink-2); font-family: var(--mono); font-size: 11px; letter-spacing: 0.08em;
  -webkit-appearance: none; appearance: none; text-align: left;
}
.daybar button.chip { min-height: 44px; padding: 8px 14px; color: var(--ink); }
/* The session you are already on, inside the picker. */
.card .btn.ghost[aria-pressed='true'] { border-color: var(--acid); color: var(--acid); }
/* Group headings inside the picker card — nothing above them, so the 26px gap is wrong. */
.card > .eyebrow { margin: 18px 0 8px; }
/* The two ways out of the picker, held apart from the sessions you could pick. */
.pickout { border-top: 1px solid var(--line-soft); margin-top: 14px; padding-top: 2px; }

/* Session bar — count on the left, the one commit button on the right. Lives inside
   #view-workout, so it exists only while a session is rendered and goes away with the
   tab. The padding below buys back the room it takes off the bottom of the page, and
   `.resting` buys the rest timer's row on top of it. */
#view-workout { padding-bottom: 62px; }
.sessbar {
  position: fixed; left: 0; right: 0; bottom: calc(var(--tab-h) + var(--safe-b)); z-index: 44;
  max-width: 760px; margin: 0 auto;
  display: flex; align-items: center; gap: 12px;
  padding: 8px var(--pad) 10px;
  background: rgba(11, 12, 14, 0.94); backdrop-filter: blur(14px);
  border-top: 1px solid var(--line);
}
.sessbar .count {
  font-size: 12px; letter-spacing: 0.08em; text-transform: uppercase; color: var(--ink-2);
}
.sessbar .btn { width: auto; margin: 0; padding: 0 18px; margin-left: auto; }

/* rest timer */
.resttimer {
  /* It sits on top of .sessbar (59px), which exists only while the Workout view is
     rendering a session — so that bar's presence is the one test for whether the timer
     has anything to float over. The picker, a rest day and every other tab have no
     .sessbar and get no timer. Hidden, never stopped: the clock keeps running while you
     check something else, and a session view brings it straight back. */
  position: fixed; left: 0; right: 0; bottom: calc(var(--tab-h) + 59px + var(--safe-b)); z-index: 45;
  max-width: 760px; margin: 0 auto;
  display: none;
}
/* One condition, both effects. The timer is on screen only while the clock is running
   (`.resting`, set with #rest's own .hide) AND a session's .sessbar is under it for it to
   float over — and the padding that buys back its row has to hang off exactly the same
   test, or a change-session / rest-day picker shows no timer and still reserves 147px of
   dead space under the card. The selector is written once per property, on purpose: they
   are two properties on two elements and they must never drift apart.
   147 is the same number as scroll-padding-bottom above — the scroll margin is only
   reachable if the page has that much slack under the last set row to scroll into. */
#app:has(#view-workout:not(.hide).resting .sessbar) .resttimer { display: block; }
#app:has(#view-workout:not(.hide).resting .sessbar) #view-workout { padding-bottom: 147px; }
.resttimer .inner {
  margin: 0 var(--pad) 10px; background: var(--surface-2);
  border: 1px solid var(--acid-dim); border-radius: var(--r-s);
  display: flex; align-items: center; gap: 12px; padding: 10px 14px;
  box-shadow: 0 8px 30px rgba(0, 0, 0, 0.5);
}
.resttimer .t { font-family: var(--mono); font-size: 22px; font-weight: 600; color: var(--acid); }
.resttimer .lab { font-size: 12px; color: var(--ink-2); flex: 1; }
.resttimer button { font-size: 12px; color: var(--ink-3); min-height: 44px; padding: 8px 12px; }
.resttimer .bar { position: absolute; left: var(--pad); right: var(--pad); bottom: 10px; height: 2px; background: var(--acid); border-radius: 2px; }

/* --------------------------------------------------------------- tables */

.wtable { width: 100%; border-collapse: collapse; font-family: var(--mono); font-size: 12.5px; }
.wtable th {
  text-align: right; font-weight: 500; color: var(--ink-3); font-size: 11px;
  letter-spacing: 0.08em; text-transform: uppercase; padding: 6px 4px; border-bottom: 1px solid var(--line);
}
.wtable th:first-child, .wtable td:first-child { text-align: left; }
.wtable td:first-child, .wtable th:first-child { white-space: nowrap; }
/* Phones: drop the per-day columns, keep Week / Avg / Dif. Scoped to the weeks
   table — the Compare table is also .wtable and only has three columns. */
@media (max-width: 460px) {
  .wtable.weeks th:nth-child(n+2):nth-child(-n+8),
  .wtable.weeks td:nth-child(n+2):nth-child(-n+8) { display: none; }
}
.wtable td { text-align: right; padding: 9px 4px; border-bottom: 1px solid var(--line-soft); color: var(--ink-2); }
.wtable td.avg { color: var(--ink); font-weight: 600; }
.wtable td.up { color: var(--up); }
.wtable td.down { color: var(--down); }
.wtable td.none { color: #4a505a; }
/* A cell that carries a date under its value stacks it — "Garage day Sat, Sep 20" on one
   line reads as one string, and at 390px it wraps wherever it likes. */
.wtable td small { display: block; font-size: 11px; color: var(--ink-3); margin-top: 2px; }

.spark { width: 100%; height: 84px; display: block; }
.spark path { fill: none; stroke: var(--acid); stroke-width: 2; stroke-linejoin: round; stroke-linecap: round; }
.spark .fill { fill: rgba(var(--acid-rgb), 0.08); stroke: none; }
.spark .grid { stroke: var(--line); stroke-width: 1; }

.stat { display: grid; grid-template-columns: repeat(2, 1fr); gap: 10px 20px; }
.stat > div {
  background: none; border: 0; border-top: 1px solid var(--line);
  border-radius: 0; padding: 12px 0 14px;
}
.stat .k {
  font-family: var(--mono); font-size: 11px; letter-spacing: 0.12em; text-transform: uppercase;
  color: var(--ink-3);
}
.stat .v { font-family: var(--mono); font-size: 26px; font-weight: 600; letter-spacing: -0.02em; margin-top: 4px; }
.stat .v small { font-size: 13px; color: var(--ink-3); font-weight: 400; }
.stat .v.up { color: var(--up); } .stat .v.down { color: var(--down); }
/* An odd tile out takes the whole row rather than sitting beside a hole. */
.stat > div:last-child:nth-child(odd) { grid-column: 1 / -1; }

.list { display: flex; flex-direction: column; }
.list .item {
  display: flex; align-items: center; gap: 12px; padding: 13px 0;
  border-bottom: 1px solid var(--line-soft);
}
.list .item .grow { flex: 1; min-width: 0; }
.list .item .grow b { display: block; font-weight: 600; font-size: 15px; }
/* Name and its badge on one line — who() makes the name a block-level <b>. */
.list .item .grow .nm { display: flex; align-items: center; gap: 7px; }
.list .item .grow .nm b { display: inline; }
/* `overflow-wrap` is the backstop for the glued separator (see `bullets`): a clause pair is
   one unbreakable token, so a row whose clause is a single long word — a custom workout
   label is up to 60 characters of whatever somebody typed — printed 48px outside this
   column and over the numbers beside it. Measured at 390 in a 211px column: a 29-character
   one-word label alone fits, the same label glued to its date did not. Every real value in
   the app still breaks at a space, so this only ever fires on a run-on word, where a break
   inside it beats printing over the neighbour. */
.list .item .grow span { font-size: 12px; color: var(--ink-3); font-family: var(--mono); overflow-wrap: anywhere; }
.list .item .r { font-family: var(--mono); font-size: 13px; color: var(--ink-2); text-align: right; }
/* A row action is a tap target, not a label — the same 44px every other chip in the app
   gets. Archive is the quieter of the pair: edit is what you meant, archive is the exit. */
.list .item button.chip { min-height: 44px; min-width: 44px; padding: 8px 14px; color: var(--ink); }

/* Anything that takes something away, or that somebody else has to undo on their phone.
   It is quieter than the thing beside it — a control that removes must never read like
   the one that saves — and it only goes red under the thumb, so the screen is not a
   warning light. One word on the button is the whole opt-in: this started as two rules
   about `archive` and every destructive control added since was styled as benign. */
.btn.danger, .list .item button.chip.danger { color: var(--ink-3); }
.btn.danger:active, .list .item button.chip.danger:active { color: var(--down); }
/* The slot number is a badge hugging its select, and the select fills the rest of the
   row — no dead gap between a number and its value. Seven fat rows would eat the fold. */
.list .item.dayrow { padding: 7px 0; }
.list .item.dayrow .slot {
  flex: none; width: 28px; height: 28px; display: grid; place-items: center;
  border: 1px solid var(--line); border-radius: 50%;
  font-family: var(--mono); font-size: 12px; font-weight: 600; color: var(--ink-3);
}

.flag {
  border-left: 2px solid var(--warn); background: rgba(255, 194, 71, 0.06);
  padding: 11px 13px; border-radius: 0 var(--r-s) var(--r-s) 0; font-size: 13.5px; color: #ffdc93;
  margin-top: 8px; line-height: 1.4;
}
.flag.good { border-color: var(--acid); background: rgba(var(--acid-rgb), 0.06); color: var(--acid); }
.flag.bad { border-color: var(--down); background: rgba(255, 106, 92, 0.07); color: #ffb3ab; }
.flag b { font-weight: 700; }

.recap { border-top: 1px solid var(--line); padding-top: 12px; margin-bottom: 18px; }
/* :not(.eyebrow) or this beats .eyebrow on specificity and order, and the recap's
   own eyebrow renders as body prose. */
.recap p:not(.eyebrow) { font-size: 14px; line-height: 1.5; color: var(--ink-2); margin: 0 0 8px; max-width: 62ch; }
/* p.compliance, not .compliance: the rule above is now (0,2,1) and would outrank it. */
.recap p.compliance { font-size: 11.5px; color: var(--ink-3); }
.recap.stale { opacity: 0.75; }

/* Doctrine you consult, not something the app noticed about you — so it takes the
   read-only hairline, never .flag. A flag that renders on every session is wallpaper,
   and the next real one inherits that indifference. Collapsed by default: you read
   this once, on session one, and the screen underneath is used every 90 seconds. */
.doctrine { border-top: 1px solid var(--line); margin: 20px 0; }
.doctrine > summary {
  min-height: 44px; display: flex; align-items: center; justify-content: space-between;
  font-family: var(--display); font-size: 12.5px; font-weight: 500;
  letter-spacing: 0.06em; text-transform: uppercase; color: var(--ink-3);
  cursor: pointer; list-style: none;
}
.doctrine > summary::-webkit-details-marker { display: none; }
/* Decoration, the same as .photobtn's plus: the summary already says "Warm-up ramp" and
   the disclosure state is on the element. Without the alt-text form the marker lands in
   the accessible name and it reads out as "Warm-up ramp plus". */
.doctrine > summary::after { content: '+'; content: '+' / ''; font-family: var(--mono); font-size: 14px; }
.doctrine[open] > summary::after { content: '–'; content: '–' / ''; }
.doctrine p {
  max-width: 62ch; margin: 0 0 14px; font-size: 13.5px; line-height: 1.5; color: var(--ink-2);
}
.doctrine .unit {
  font-family: var(--mono); font-size: 11px; letter-spacing: 0.08em; color: var(--ink-3);
  border: 1px solid var(--line); border-radius: 3px; padding: 2px 5px; margin: 0 6px 0 2px;
}
/* Wraps between pairs, never inside one — the non-breaking spaces live in the string. */
.doctrine .mono { font-family: var(--mono); }

/* A caution is about this person's body, not their data — it sits with the exercise,
   not in a .flag, because it is true every session and a flag that never turns off
   stops meaning anything. */
.ex-care {
  margin: 0 14px 8px 44px; padding: 9px 11px 9px 9px;
  border-left: 2px solid var(--warn); background: rgba(255, 194, 71, 0.06);
  border-radius: 0 var(--r-s) var(--r-s) 0;
  font-size: 12.5px; line-height: 1.45; color: #ffdc93;
}
.ex-find {
  padding: 0 14px 8px 44px; margin: 0;
  font-family: var(--display); font-size: 12.5px; line-height: 1.45; color: var(--ink-2);
}
.ex-find b { color: var(--acid); font-weight: 700; }
/* Never let a lightened number be silent. */
.target .cut {
  display: block; font-family: var(--mono); font-size: 10.5px; color: var(--warn);
}

.photowall { display: grid; grid-template-columns: repeat(3, 1fr); gap: 6px; }
.photowall img, .photowall .empty { aspect-ratio: 3/4; width: 100%; object-fit: cover; border-radius: var(--r-s); background: var(--surface-2); }
.photowall .empty {
  display: grid; place-items: center; border: 1px dashed var(--line);
  font-family: var(--mono); font-size: 10.5px; color: var(--ink-3);
}
.photowall .empty::after { content: 'no photo'; }
/* --------------------------------------------------------------- forms */

input[type='checkbox'] { accent-color: var(--acid); }
.field { margin-top: 12px; }
.field label {
  display: block; font-family: var(--mono); font-size: 11px; letter-spacing: 0.12em;
  text-transform: uppercase; color: var(--ink-3); margin-bottom: 6px;
}
.field input, .field select, .field textarea {
  width: 100%; min-height: var(--tap); padding: 10px 12px;
  background: var(--surface-2); border: 1px solid var(--line); border-radius: var(--r-s);
}
.field textarea { min-height: 76px; resize: vertical; }
.field input:focus, .field select:focus, .field textarea:focus { outline: none; border-color: var(--acid); }
.grid2 { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; }
/* iOS Safari gives date inputs an intrinsic min-width that ignores width:100% and pushes
   past the card edge; it also centres the value. A wrapped label ("Sex (photo visibility)")
   used to drop that cell's control below its neighbour — pin controls to the cell bottom. */
.grid2 > .field { min-width: 0; display: flex; flex-direction: column; }
.grid2 > .field > :not(label) { margin-top: auto; }
.field input[type='date'] {
  display: block; min-width: 0; -webkit-appearance: none; appearance: none; text-align: left;
}
.field input[type='date']::-webkit-date-and-time-value { text-align: left; }

/* --------------------------------------------------------------- login */

.login { min-height: 100dvh; display: grid; place-items: center; padding: 24px; }
.login .box { width: 100%; max-width: 360px; }
.login h1 { font-size: 13px; letter-spacing: 0.3em; text-transform: uppercase; text-align: center; }
.login h1 b { color: var(--acid); }
.login p.sub { text-align: center; color: var(--ink-3); font-size: 13px; margin: 8px 0 26px; }

/* The colour picker reuses .login's centred box, but has to cover an already-rendered
   app rather than replace it — hence fixed + its own background. */
#picker { position: fixed; inset: 0; z-index: 80; background: var(--bg); overflow-y: auto; }
#picker .box { max-width: 420px; }
#picker h1 { font-size: 24px; letter-spacing: -0.02em; text-transform: none; }
#picker p.sub { margin-bottom: 20px; line-height: 1.5; font-size: 13.5px; color: var(--ink-2); }

.swatches { display: grid; grid-template-columns: repeat(4, 1fr); gap: 8px; }
.swatch {
  display: grid; justify-items: center; gap: 6px; padding: 10px 0 8px;
  background: var(--surface); border: 1px solid var(--line); border-radius: var(--r-s);
}
.swatch i { width: 34px; height: 34px; border-radius: 50%; }
.swatch span {
  font-family: var(--mono); font-size: 11px; letter-spacing: 0;
  text-transform: uppercase; color: var(--ink-3); text-align: center;
  max-width: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.swatch[aria-pressed='true'] { border-color: var(--ink-2); background: var(--surface-2); }
.swatch[aria-pressed='true'] span { color: var(--ink); }
.swatch[aria-pressed='true'] i { box-shadow: 0 0 0 2px var(--bg), 0 0 0 4px currentColor; }
.swatch[disabled] { opacity: 0.4; }
.swatch[disabled] i { transform: scale(0.72); }

.err { color: var(--down); font-size: 13px; margin-top: 10px; min-height: 18px; }

.toast {
  position: fixed; left: 50%; transform: translateX(-50%); bottom: calc(130px + var(--safe-b));
  background: var(--ink); color: var(--bg); font-size: 13px; font-weight: 600;
  padding: 10px 16px; border-radius: 100px; z-index: 60; pointer-events: none;
  opacity: 0; transition: opacity 0.18s;
}
.toast.on { opacity: 1; }

.saving {
  position: fixed; top: auto; bottom: calc(120px + var(--safe-b)); right: 18px; z-index: 60;
  font-family: var(--mono); font-size: 11px; letter-spacing: 0.1em; text-transform: uppercase;
  color: var(--ink-3); opacity: 0; transition: opacity 0.2s;
}
.saving.on { opacity: 1; }
.saving.err { color: var(--down); opacity: 1; }
/* A write the server refused outright. It says so in a sentence, so it needs room to
   wrap — and it does not go away on its own the way "retrying" does. Tapping it
   is the way to throw the refused change away, so it has to look tappable.
   Docked onto the tab bar with the rest of the bottom furniture, rather than floated in
   the right of the column: a 62vw box growing upwards off the transient indicator's
   anchor landed in the middle of the check-in form, over whichever row happened to be
   there. Nothing that stays on screen can avoid overlapping SOMETHING as the page moves
   under it — the tab bar, the session bar and the rest timer are all in the same
   position — so it joins them at the bottom edge and buys back its row, instead of
   reading as a box dropped on the form. */
.saving.stuck {
  cursor: pointer;
  left: var(--pad); right: var(--pad); max-width: 760px; margin: 0 auto;
  bottom: calc(var(--tab-h) + 6px + var(--safe-b));
  min-height: 44px; display: flex; align-items: center;
  white-space: normal; text-align: left;
  text-transform: none; letter-spacing: 0.01em; line-height: 1.35;
  background: var(--surface-2); border: 1px solid var(--down);
  border-radius: var(--r-s); padding: 8px 11px;
}
/* The session bar holds the one button that finishes a workout. Step over it, the same
   way the rest timer does, on the same test — the bar's presence. */
#app:has(#view-workout:not(.hide) .sessbar) .saving.stuck {
  bottom: calc(var(--tab-h) + 59px + 6px + var(--safe-b));
}
/* And buy back the room it takes, the way .sessbar and the rest timer do — otherwise the
   last control on the page sits under it for as long as the refusal stands, which for a
   notice that never times out is for good. */
#app:has(#saving.stuck) .shell { padding-bottom: calc(220px + var(--safe-b)); }

@media (prefers-reduced-motion: reduce) { * { transition: none !important; animation: none !important; } }

/* --------------------------------------------------------------- exercise demos */
/* "?" beside every lift and stretch → bottom sheet with the movement's start/end frames
   (or a self-filmed clip). Same 44px hit area trick as the chips. */
.demo-btn {
  flex: none; width: 26px; height: 26px; border-radius: 50%; position: relative;
  border: 1px solid var(--line); background: var(--surface-2); color: var(--ink-2);
  font-family: var(--mono); font-size: 12px; font-weight: 700; line-height: 1;
}
.demo-btn::after { content: ''; position: absolute; inset: -9px; }
.demo-btn:active { border-color: var(--acid); color: var(--acid); }

.demo {
  position: fixed; inset: 0; z-index: 90; background: rgba(0, 0, 0, 0.78);
  backdrop-filter: blur(2px); -webkit-backdrop-filter: blur(2px);
  display: flex; align-items: flex-end; justify-content: center;
}
.demo-card {
  width: 100%; max-width: 560px; max-height: 88dvh; overflow-y: auto;
  background: var(--surface); border: 1px solid var(--line); border-bottom: 0;
  border-radius: 16px 16px 0 0; padding: 16px 16px calc(16px + var(--safe-b));
  animation: demo-up 220ms ease-out;
}
/* Wide screens: a bottom-glued 560px card reads as clipped — float it instead. */
@media (min-width: 640px) {
  .demo { align-items: center; }
  .demo-card { border-radius: 16px; border-bottom: 1px solid var(--line); animation: none; }
}
@keyframes demo-up { from { transform: translateY(24px); opacity: 0; } to { transform: none; opacity: 1; } }
.demo-head { display: flex; align-items: flex-start; justify-content: space-between; gap: 12px; margin-bottom: 12px; }
.demo-head button.chip { min-height: 44px; padding: 8px 14px; color: var(--ink); }   /* only exit — real 44px */
.demo-head h3 { font-size: 18px; letter-spacing: -0.01em; }
.demo-sub { font-size: 11px; color: var(--ink-3); margin-top: 4px; letter-spacing: 0.06em; }
.demo-gif { display: block; width: 100%; aspect-ratio: 1; object-fit: contain; background: #fff; border-radius: var(--r-s); }
.demo-frames {
  display: block; width: 100%; aspect-ratio: 3 / 2; border-radius: var(--r-s);
  background: #fff; overflow: hidden; position: relative;
}
.demo-frames img { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: contain; }
/* Two frames, start ↔ end, ~1 s each — reads as a slow loop of the rep. */
.demo-frames img + img { animation: demo-flip 2.2s steps(1) infinite; }
@keyframes demo-flip { 0%, 49% { opacity: 0; } 50%, 100% { opacity: 1; } }

/* --------------------------------------------------------------- own workouts */
/* The movement picker reuses the demo sheet; only the search box and the rows are new. */
#mvq {
  width: 100%; min-height: var(--tap); padding: 10px 12px; margin-bottom: 4px;
  background: var(--surface-2); border: 1px solid var(--line); border-radius: var(--r-s);
}
#mvq:focus { outline: none; border-color: var(--acid); }
/* A thin dark thumb instead of the OS's white slab, and room under the last row so it is
   not clipped in half at the fold of the sheet. The 24 is inside the scroller, on top of
   the sheet's own 16 + safe-area padding — the list is glued to the sheet's bottom, so
   this padding is the only thing that buys air under the last row. */
#mvList {
  max-height: 52dvh; overflow-y: auto; padding-bottom: 24px;
  scrollbar-width: thin; scrollbar-color: var(--line) transparent;
}
#mvList::-webkit-scrollbar { width: 8px; }
#mvList::-webkit-scrollbar-track { background: transparent; }
#mvList::-webkit-scrollbar-thumb { background: var(--line); border-radius: 100px; }
.list button.item {
  width: 100%; text-align: left; background: none; border: 0;
  border-bottom: 1px solid var(--line-soft); min-height: var(--tap);
}
.list button.item:active b { color: var(--acid); }
/* "Use what I typed" is a different kind of row from the ebook's names — the term you
   typed carries the accent, and a full-strength rule separates it from the list. */
.list button.item.own { border-bottom-color: var(--line); }
.list button.item.own i { font-style: normal; color: var(--acid); }

/* Name, sets, rep range, remove — on one row at 390px, name on its own line below that,
   or a long name squeezes the three controls into a different shape on every row. */
.wmove select, .wmove input {
  flex: none; min-height: 44px; padding: 6px 8px; text-align: center;
  background: var(--surface-2); border: 1px solid var(--line); border-radius: var(--r-s);
  color: var(--ink); font-size: 13px;
}
.wmove select { width: 64px; }
.wmove input { width: 74px; }
.wmove select:focus, .wmove input:focus { outline: none; border-color: var(--acid); }
/* .list .item .grow sets flex:1 — match its specificity or the name never takes the line. */
@media (max-width: 460px) {
  .wmove { flex-wrap: wrap; }
  .list .item.wmove .grow { flex: 1 0 100%; }
  /* Same shape, same reason, for a People row: two chips leave the text 184px, and its
     meta line does not fit in that at any split. The text takes the row and the chips
     drop below it, where they cannot be printed through. */
  .list .item.person { flex-wrap: wrap; }
  .list .item.person .grow { flex: 1 0 100%; }
  .list .item.person > button.chip { margin-top: 6px; }
}
/* The meta is two lines on purpose — see adminPanel. Block, so the second never runs on
   from the first without a separator between them. */
.list .item.person .grow > span { display: block; }
/* The builder belongs to the row you tapped, not to the card stack it landed in. The
   scroll margin is the sticky topbar: without it "block: start" parks the card's first
   field underneath it. */
#wEdit { border-color: var(--acid-dim); scroll-margin-top: calc(var(--top-h) + 10px); }

/* An empty session and a filled one take the same slot: the invitation sits in a card
   where the movement list would be, not naked on the background. */
.card.empty { margin-top: 12px; text-align: center; }
.card.empty p { margin: 0; color: var(--ink-2); font-size: 13.5px; }
.card.empty .btn { max-width: 260px; margin: 12px auto 0; }
/* On a phone a full-width "+ Add movement" is the button; on a desktop it is a banner. */
@media (min-width: 620px) { .btn.slim[data-act^='addMove'] { max-width: 260px; } }
.item select[data-week] { flex: 1; min-width: 0; max-width: 360px; min-height: 44px; padding: 6px 10px;
  background: var(--surface-2); border: 1px solid var(--line); border-radius: var(--r-s); color: var(--ink); }
